Contents
Developers Reference
Reference
Every endpoint, status, error code and webhook event. The endpoints are generated from the OpenAPI 3.1 contract, so this page cannot describe a parameter the API does not have.
Endpoints
Grouped as the contract groups them. A * marks a required field. Paths are relative to https://www.payshen.com.
Events
get/api/v1/events
List events across all payments
The polling equivalent of your webhook endpoint. Scoped to the key's environment.
Field Type Notes type string queryExact event type, e.g. payment.captured.before string queryCursor: the last id from the previous page. limit integer ≥ 1 query - 200
- A page of events.
- 401
- Missing, malformed, revoked or unknown API key.
- 403
- The workspace is suspended, or the key lacks the required scope.
- 422
validation_error.- 429
- Rate limited. Retry after the interval in the Retry-After header.
get/api/v1/payments/{id}/events
List a payment's timeline
Field Type Notes id* string pathThe payment id returned as payment_idon creation.- 200
- Events, oldest first.
- 401
- Missing, malformed, revoked or unknown API key.
- 403
- The workspace is suspended, or the key lacks the required scope.
- 404
not_found.- 429
- Rate limited. Retry after the interval in the Retry-After header.
Partners
get/api/v1/merchants
List your sub-merchants
- 200
- The partner's merchants.
- 401
- Missing, malformed, revoked or unknown API key.
- 403
not_a_partner.- 429
- Rate limited. Retry after the interval in the Retry-After header.
post/api/v1/merchants
Provision a sub-merchant
White-label partners only. Requires a live key and the partner flag on your workspace, both enforced server-side.
- 201
- The sub-merchant workspace was created and its owner invited.
- 401
- Missing, malformed, revoked or unknown API key.
- 403
not_a_partner, orlive_key_requiredwhen called with a test key.- 409
already_invited.- 422
validation_error.- 429
merchant_limit- the partner's allowance is exhausted.
Payments
get/api/v1/openapi.jsonno auth
This document
The machine-readable contract, unauthenticated so it can be read and imported before you have an account.
- 200
- An OpenAPI 3.1 document.
get/api/v1/payments
List payments
Newest first, cursor paginated on payment id. Scoped to the key's environment.
Field Type Notes status string queryCase-insensitive. before string queryCursor: the last id from the previous page. limit integer ≥ 1 query - 200
- A page of payments.
- 401
- Missing, malformed, revoked or unknown API key.
- 403
- The workspace is suspended, or the key lacks the required scope.
- 422
validation_error- an unknown status filter.- 429
- Rate limited. Retry after the interval in the Retry-After header.
post/api/v1/payments
Create a payment
Routes the payment across your active connections. A second connection is tried only after a soft decline or a technical error before anything was executed (per routing mode), never after a timeout or an unknown result, which answers
UNKNOWN. Withreturn_urlthe response is a redirect instead of an outcome.Field Type Notes Idempotency-Key string header8-200 printable ASCII characters. Scoped to your workspace and environment. Required when the API key's environment is production (a ps_live_key): such a request without it is refused 400idempotency_key_required. Optional in sandbox (ps_test_). Once a provider may have received the request, every retry under this key is answered from what Payshen recorded and never sent to a provider again: the stored response, else the payment's current state, elseUNKNOWN. The key is kept 24 hours after its answer, and for as long as the provider attempt has no answer or its payment isUNKNOWN; after that the same key is a new request.amount* integer ≥ 50 Minor units of currency. 5000 is EUR 50.00, and JPY 5,000.currency* string ISO 4217. country* string ISO 3166-1 alpha-2 of the customer. method "card" | "bank" | "wallet" | "crypto" card_brand string reference string Your own identifier. Echoed back and in webhooks. initiator "cit" | "mit" mitis merchant-initiated, e.g. recurring.capture_method "automatic" | "manual" return_url string Present to use the hosted redirect flow. cancel_url string debug boolean When true, routing.excludedlists every connection removed before routing, with acodeand areason, and arouting_failederror carries the same list. Off by default: the list names your connections and their approval terms. Ignored on the hosted redirect branch, which does not route until the customer acts.- 201
- Created. Either an outcome, or a redirect to complete it.
- 400
idempotency_key_required(a live key sent no Idempotency-Key),invalid_idempotency_keyorinvalid_json.- 401
- Missing, malformed, revoked or unknown API key.
- 403
- The workspace is suspended, or the key lacks the required scope.
- 409
idempotency_in_progress, orrouting_failedwhen no connection can take it. Withdebug: truearouting_failederror also carrieserror.excluded, the same list asrouting.excluded.- 413
payload_too_large- the body exceeds 16 KB.- 422
validation_error, oridempotency_key_reusefor a reused key with a different body.- 429
- Rate limited. Retry after the interval in the Retry-After header.
- 500
internal_error- nothing was sent to a provider; safe to retry with the same Idempotency-Key.payment_result_unknown- a provider may have received the payment and its result could not be recorded yet; the error carriesrequest_id. Do not retry with a new Idempotency-Key. A retry with the same key never charges again: it answers 409 while the first request's claim is fresh (15 minutes), then the payment as Payshen recorded it (oftenUNKNOWN). A daily job records such a payment even if nobody retries.
get/api/v1/payments/{id}
Retrieve a payment
Field Type Notes id* string pathThe payment id returned as payment_idon creation.- 200
- The payment.
- 401
- Missing, malformed, revoked or unknown API key.
- 403
- The workspace is suspended, or the key lacks the required scope.
- 404
not_found. Also returned for a payment belonging to another workspace or environment.- 429
- Rate limited. Retry after the interval in the Retry-After header.
post/api/v1/payments/{id}/capture
Capture an authorization
Full or partial. Only a payment created with
capture_method: manualcan be captured.Field Type Notes id* string pathThe payment id returned as payment_idon creation.amount integer ≥ 1 Minor units. Omit to capture the full authorization. - 200
- Captured.
- 400
invalid_idempotency_key- the Idempotency-Key is not 8-200 printable ASCII characters.- 401
- Missing, malformed, revoked or unknown API key.
- 403
- The workspace is suspended, or the key lacks the required scope.
- 404
not_found.- 409
not_capturable- the payment is notAUTHORIZED(already captured, voided, or a capture or void is in flight or unknown);capture_conflict- another capture or void took it first;no_provider_ref- there is no provider reference to capture;idempotency_in_progress- a request with this Idempotency-Key is still being processed.- 422
validation_errororinvalid_amount- the amount is not a positive integer or exceeds the authorization;idempotency_key_reuse- the key was used with a different amount.- 429
- Rate limited. Retry after the interval in the Retry-After header.
- 500
state_unconfirmed- the provider was asked and its answer could not be recorded (the message says whether it confirmed, refused or did not answer);state_conflict- the provider confirmed the capture and the payment had meanwhile moved. Do not retry: the same Idempotency-Key replays this answer, and the payment is checked with the provider by Payshen operations.- 501
unsupported- the payment's provider cannot capture.- 502
psp_error- the provider refused the capture (the message is the provider's): nothing was captured and the payment isAUTHORIZEDagain; the Idempotency-Key is released, so a new attempt is safe.capture_result_unknown- the provider did not answer and may have captured: the payment isUNKNOWN. Do NOT retry; the same Idempotency-Key replays this answer, and Payshen settles the payment from the provider's record.
post/api/v1/payments/{id}/refunds
Refund a captured payment
Full or partial. The amount is clamped to the refundable balance, and concurrent full refunds cannot both succeed. Send an
Idempotency-Key: on this endpoint it names the refund itself, so a request repeated under the same key is answered from that refund and never refunds twice. For 24 hours the stored response is replayed as it was sent; after that, the same key on the same payment is answered from the refund's current record (its final result once it is settled). A refund the provider has not confirmed yet is counted inrefund_pending_amount, not inrefunded_amount, and is settled from the provider's own record: a refund still pending is never answered as refunded.Field Type Notes id* string pathThe payment id returned as payment_idon creation.Idempotency-Key string header8-200 printable ASCII characters. The same key on the same payment is the same refund: a repeat answers that refund's result (or 409 refund_pendingwhile the provider has not confirmed it) and never reaches the provider again. Within 24 hours a repeat replays the stored response; after 24 hours it is answered from the refund's record. The same key with a different amount is refused.amount integer ≥ 1 Minor units. Omit to refund the full refundable balance. reason string - 200
- Refunded: the provider confirmed it.
- 401
- Missing, malformed, revoked or unknown API key.
- 403
- The workspace is suspended, or the key lacks the required scope.
- 404
not_found.- 409
refund_pending- this refund, or one holding the rest of the balance, is not confirmed yet: the provider accepted it and is still processing it, or its answer is still being settled. It is not refunded, and its amount is inrefund_pending_amount. Do not retry with a new key; the same Idempotency-Key answers its final result once it is settled. Alsonot_refundableandrefund_conflict.- 422
validation_errororinvalid_amount- nothing refundable, or an invalid amount;idempotency_key_reuse- the key was used with a different amount.- 429
- Rate limited. Retry after the interval in the Retry-After header.
- 500
state_unconfirmed- the provider confirmed the refund, and either recording it failed or the payment's status could not be updated after it (the message says which). Do not retry: the same Idempotency-Key replays this answer for 24 hours, and after that is answered from the refund's record.- 502
psp_error- the provider refused the refund: nothing was refunded and the amount is refundable again. The same Idempotency-Key answers the same refusal; a retry with a NEW Idempotency-Key is a new refund and is safe.refund_result_unknown- the provider did not answer and may have refunded: the amount stays inrefund_pending_amount. Do NOT retry, with any key; Payshen settles it from the provider's record and the payment shows the result.
post/api/v1/payments/{id}/void
Void an authorization
Releases an uncaptured authorization. A captured payment must be refunded instead.
Field Type Notes id* string pathThe payment id returned as payment_idon creation.- 200
- Voided.
- 400
invalid_idempotency_key- the Idempotency-Key is not 8-200 printable ASCII characters.- 401
- Missing, malformed, revoked or unknown API key.
- 403
- The workspace is suspended, or the key lacks the required scope.
- 404
not_found.- 409
not_voidable- the payment is notAUTHORIZED(already captured, voided, or a capture or void is in flight or unknown);void_conflict- another capture or void took it first;no_provider_ref- there is no provider reference to void;idempotency_in_progress- a request with this Idempotency-Key is still being processed.- 429
- Rate limited. Retry after the interval in the Retry-After header.
- 500
state_unconfirmed- the provider was asked and its answer could not be recorded (the message says whether it confirmed, refused or did not answer);state_conflict- the provider confirmed the void and the payment had meanwhile moved. Do not retry: the same Idempotency-Key replays this answer, and the payment is checked with the provider by Payshen operations.- 501
unsupported- the payment's provider cannot void.- 502
psp_error- the provider refused the void (the message is the provider's): the payment isAUTHORIZEDagain; the Idempotency-Key is released, so a new attempt is safe.void_result_unknown- the provider did not answer and may have voided: the payment isUNKNOWN. Do NOT retry; the same Idempotency-Key replays this answer, and Payshen settles the payment from the provider's record.
Providers
post/api/v1/psp-webhooks/{pspId}no auth
Inbound provider webhook
Called by a payment provider, not by you. Requires an HMAC signature issued to that provider. Documented so integrators understand the inbound path, not so they call it.
Field Type Notes pspId* string path X-Payshen-Signature* string header t=<unix>,v1=HMAC_SHA256(secret, "<t>.<body>").- 200
- Accepted, stored and applied.
- 401
- Missing or invalid signature. An unknown provider id answers identically, by design.
- 429
- Rate limited.
Statuses
The unified status set, UPPERCASE on the wire. A payment only moves forward: once captured, settled, refunded or disputed it does not go back to an earlier state.
| Status | Meaning |
|---|---|
| CREATED | The payment exists and nothing has been attempted at a provider yet. |
| PENDING | The provider has it and is processing. In flight, not an outcome. |
| REQUIRES_ACTION | Waiting on the customer. Send them to next_action.url. |
| AUTHORIZED | Approved and held, not yet captured. Capture it or void it. |
| CAPTURED | Approved and captured at the provider. |
| DECLINED | The provider refused it. decline_category carries the normalized reason. |
| FAILED | It could not be completed, and will not be. The payment's timeline records why. |
| UNKNOWN | The provider may have executed the operation and we could not tell (a timeout after the request was sent, a connection lost mid-answer). |
| CANCELLED | Ended before any money was taken: an authorization voided, or the customer cancelled on the hosted page. |
| REFUNDED | The whole captured amount has been refunded. |
| PARTIALLY_REFUNDED | Part of the captured amount has been refunded; the rest can still be. |
| CHARGEBACK | A chargeback was recorded against it. |
| SETTLED | The captured funds appeared in a settlement report matched to this payment. |
UNKNOWN and PENDING
Treat both as in flight. Never as a decline, and never retry the capture or void yourself: the first one may already have happened. Read the payment again later; it moves to a confirmed status once the provider's own record settles the question.
Error codes
Every failure is { "error": { "code", "message" } }. Switch on code, which is stable under the v1 freeze. The message is for people and may change.
| Code | HTTP | Meaning |
|---|---|---|
| invalid_json | 400 | The body is not valid JSON. |
| idempotency_key_required | 400 | POST /v1/payments with a production (ps_live_) key sent no Idempotency-Key. Sandbox (ps_test_) keys may omit it. |
| invalid_idempotency_key | 400 | The Idempotency-Key header is not 8 to 200 printable ASCII characters. |
| unauthorized | 401 | The API key is missing, malformed, revoked or unknown. |
| tenant_suspended | 403 | The workspace is suspended. |
| not_a_partner | 403 | A partner endpoint was called by a workspace that is not a partner. |
| live_key_required | 403 | Provisioning a merchant was attempted with a test key. |
| not_found | 404 | No such payment. Also returned for a payment in another workspace or environment. |
| idempotency_in_progress | 409 | A request with the same Idempotency-Key is still being processed. Retry shortly. |
| routing_failed | 409 | No connection can take this payment. |
| already_invited | 409 | That owner already has a pending invitation. Safe to treat as done. |
| payload_too_large | 413 | The body exceeds 16 KB. |
| validation_error | 422 | A field is missing or invalid, or the payment is not in a state that allows the operation. |
| idempotency_key_reuse | 422 | The Idempotency-Key was used before with a different body. |
| rate_limited | 429 | Too many requests. Wait for the Retry-After interval. |
| merchant_limit | 429 | The partner's merchant allowance is used up. |
| internal_error | 500 | Something failed on our side. Safe to retry with the same Idempotency-Key. |
| payment_result_unknown | 500 | A provider may have received the payment and its result could not be recorded yet. Retry only with the same Idempotency-Key, never a new one: it answers 409 for up to 15 minutes, then the payment as recorded. |
A 4xx is an answer: retrying it unchanged gets the same answer. A 5xx, a timeout or a 429 is worth retrying with the same Idempotency-Key.
Webhook events
Each delivery is a JSON envelope with id, type, created and data, signed in the X-Payshen-Signature header. An endpoint can subscribe to all of these or a subset. Sandbox endpoints receive only sandbox events, production endpoints only production events.
| Event | Sent when |
|---|---|
| payment.created | A hosted payment was created and is waiting for the customer. |
| payment.approved | The payment was approved and captured in one step. |
| payment.authorized | The payment was approved with capture_method manual, and is held for capture. |
| payment.captured | An authorization was captured, in full or in part. Also sent when Payshen confirms with the provider that a payment it reported as declined, failed or cancelled was in fact collected: then corrected is true and previous_status is the status you were told. Do not charge the customer again. |
| payment.declined | The payment was declined. |
| payment.unknown | The provider did not give a usable answer to the charge, so it may have gone through. Treat it as in flight, never as declined, and do not charge again. |
| payment.failed | No connection could take the charge: each refused the request before executing it (a rejected API key, an invalid request, a rate limit). Nothing was charged, and it is not a decline. |
| payment.voided | An authorization was voided. |
| payment.cancelled | The customer cancelled on the hosted page. |
| payment.refunded | A refund was made, in full or in part. |
| payment.refund_failed | A refund that was waiting on the provider (refund_pending_amount) did not go through. Its amount is no longer reserved and was not refunded. |
| webhook.test | Someone pressed Send test event in the dashboard. It never describes a real payment. |
Deduplicate on event.id
A delivery that times out or answers 5xx is sent again, so the same event can arrive twice. Record each id and skip what you have already processed. claimWebhookEvent does this in one call.
