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, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 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, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 404
not_found.- 429
- Rate limited. Retry after the interval in the Retry-After header.
Insights
get/api/v1/insights
List insights
The ranked findings from the dashboard's Insights page, for the key's environment: approval gaps between providers, declines left without a retry, fees above your pricing rules, refund and dispute trends, provider health, routing advice. Each carries an estimate of the money involved (over payments already seen, in your reporting currency), its evidence and the dashboard path where it is fixed. Dismissed and snoozed findings are left out. Computed on request and rate limited to 20 calls a minute.
Field Type Notes range "7d" | "30d" | "90d" | "3m" | "6m" | "1y" queryThe period the findings cover. - 200
- The findings, most money first.
- 401
- Missing, malformed, revoked, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 422
validation_error(an unknown range).- 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, expired or unknown API key. An expired key's message gives the date it expired.
- 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.
countryis checked against Payshen's country gate: a country Payshen does not onboard is refused withcountry_blocked; a country under review is provisioned, and the merchant's going live and provider applications wait until Payshen clears the workspace.Field Type Notes name* string owner_email* string plan "core" | "scale" | "enterprise" country string ISO 3166-1 alpha-2 country the merchant's company is registered in. Optional; checked against the country gate. - 201
- The sub-merchant workspace was created and its owner invited.
account_reviewisapprovedwhen the partner is approved, so the merchant can go live; otherwise the merchant sends its account for review from Get live. - 401
- Missing, malformed, revoked, expired or unknown API key. An expired key's message gives the date it expired.
- 403
not_a_partner, orlive_key_requiredwhen called with a test key.- 409
already_invited.- 422
validation_error, orcountry_blockedwhen Payshen does not onboard merchants registered in the country sent.- 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, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 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" allowed_methods "card" | "bank" | "wallet" | "crypto"[] Hosted redirect flow only. Every method you will accept for this payment, in your order of preference. The hosted page offers the ones one of your connections can actually take and the customer picks; the payment starts on your first offered preference (or on method, when you also sent one and it is offered). Omit it to fix a single method, which is whatmethodalone has always done. When no connection can take any of them the create is refused422 no_eligible_method, so a checkout that could offer the customer nothing is never opened.card_brand string reference string Your own identifier. Echoed back and in webhooks. initiator "cit" | "mit" A label for routing and fee modelling: mitmarks a payment your own system started without the customer in session (for example a renewal it charges with a payment method your provider holds). Payshen stores no payment methods and never starts a charge itself; this field changes how the payment is routed and priced, not who can charge.capture_method "automatic" | "manual" return_url string Present to use the hosted redirect flow. Must be https with a live key (ps_live_); a test key may use http. cancel_url string Must be https with a live key (ps_live_); a test key may use http. 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.ui "redirect" | "embedded" How the customer supplies the payment method. redirect(default, unchanged) returnsnext_action.REDIRECTto a hosted page.embeddedroutes now and returns acollector(the routed provider's browser fields or a native action) plus aclient_secret, for the Checkout SDK to mount in your page; the card goes browser -> provider and never through Payshen, and you complete it withPOST /v1/payments/{id}/confirm. Requiresreturn_url. Additive and opt-in.customer object Optional; hosted redirect flow only. The person paying, for the providers whose own checks require it before they take a payment (some high-risk and iGaming providers: Praxis needs name, email and country; ZotaPay name, email, phone, address and IP; MiFinity also date of birth and nationality; dLocal and EBANX name, email and national document). Payshen sends it only to the provider the customer is sent to, keeps it sealed on the payment session until then and deletes it as soon as the customer has been handed over or the session ends. It is never stored on the payment, never returned by the API and never put in a webhook. Leave it out for providers that do not need it: a payment routed to a provider that requires a field you did not send is refused before anything reaches the provider, naming the missing fields. - 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, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 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.no_eligible_method- the hosted flow was asked for methods no active connection can take; the message names each one and why, so the checkout is never opened with nothing to offer.return_url_required-ui: "embedded"was sent withoutreturn_url.no_eligible_connection-ui: "embedded"and no connection can take this payment right now; the message says why.pan_not_accepted- a field (a reference, metadata, a description) holds a card number; Payshen never accepts card data.return_url_not_allowed- with a live key, return_url or cancel_url points at a host that is not on the workspace's allowed return hosts (set in the dashboard under Developers; off unless the merchant turns it on).- 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, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 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 (
CAPTURED). OrPENDINGwhen the provider accepted the capture and reports its result later (Adyen, Solidgate): nothing is captured yet, andpayment.capturedfollows when the provider confirms it. - 400
idempotency_key_required- aps_live_key sent no Idempotency-Key (required in production, optional in sandbox);invalid_idempotency_key- the Idempotency-Key is not 8-200 printable ASCII characters.- 401
- Missing, malformed, revoked, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 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.simulated_payment- a live payment a simulation answered (no provider holds it, no money moved): on a production deployment a simulation never answers an operation on a live payment, so nothing was sent and nothing changed.- 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.- 503
provider_unavailable- the payment is held by a real provider that this deployment cannot reach (the connection's credentials are missing, or live provider access is off), so the capture was NOT sent and nothing changed. Payshen never lets a simulation answer for a real provider's payment. Retry once the connection reaches its provider; the Idempotency-Key is released, so a new attempt is safe.
post/api/v1/payments/{id}/complete
Complete an embedded payment after customer authentication
After the customer's 3-D Secure challenge (a confirm that answered
REQUIRES_ACTION). A BROWSER call, authenticated like confirm by the sessionclient_secret. It takes no payment method and never charges: Payshen asks the provider, with your connection's own credentials, what became of the payment and applies the provider's answer -CAPTURED,AUTHORIZED(manual capture),DECLINED(the customer failed the challenge,decline_reasone.g.payment_intent_authentication_failure) orCANCELLED- after checking its amount and currency against the payment. StillREQUIRES_ACTIONwith a freshnext_actionwhile the customer has not finished. Idempotent and safe to repeat: the provider's own webhook may settle the payment first, and whichever arrives second changes nothing. The merchant is told the outcome once, bypayment.captured,payment.authorized,payment.declinedorpayment.cancelled.Field Type Notes id* string pathThe payment id returned as payment_idon creation.client_secret* string The session client secret from the embedded create response - the same one confirm took. - 200
- The payment's authoritative state, as the provider reported it.
- 404
not_found- no payment matches this id and client_secret.- 422
validation_error- the body is malformed.pan_not_accepted- card data was sent.not_confirmable- this payment was not created for embedded confirmation.- 429
rate_limited- too many completion attempts.
post/api/v1/payments/{id}/confirm
Confirm an embedded payment
The embedded Checkout SDK's confirm. A BROWSER call, authenticated by the session
client_secretfrom an embedded create response (ui: "embedded"), NOT theps_live_API key - which must never ship to the browser. It accepts ONLY a provider-issued token viapayment_method; raw card data (PAN, CVV, expiry) is refused withpan_not_accepted, because Payshen never receives or transmits card data. Idempotent: a retry after a lost response returns the authoritative payment state without charging again. The charge is bound to the provider chosen at session-init and never fails over to another provider. When the card issuer requires Strong Customer Authentication (3-D Secure), the answer isREQUIRES_ACTIONwithnext_action(type: AUTHENTICATE): nothing is charged yet, it is not a decline, and the payment is never sent to another provider. Complete the challenge, then callPOST /v1/payments/{id}/complete. Calling confirm again instead is equivalent: it never charges a second time, it applies the provider's outcome or hands back what the customer still needs. A challenge nobody finishes is cancelled at the provider after an hour and the payment endsCANCELLED(payment.cancelled, reasonauthentication_abandoned).Field Type Notes id* string pathThe payment id returned as payment_idon creation.client_secret* string The session client secret from the embedded create response. Bound to this payment; short-lived; not the API key. payment_method* string The token the provider's hosted field returned in the browser (e.g. a Stripe pm_). Never a card number.- 200
- The payment's authoritative state after the charge, or recovered on an idempotent retry (
Idempotent-Replay: true).REQUIRES_ACTIONwithnext_actionwhen the customer must authenticate first. - 404
not_found- no payment matches this id and client_secret (a wrong id and a wrong secret answer identically; the endpoint is not an oracle).- 409
provider_unavailable- the provider this payment was bound to at session-init is no longer available, and it was NOT re-routed. Create a new payment.- 410
session_expired- this payment session has expired.- 422
pan_not_accepted- raw card data was sent; send only the provider token.validation_error- the body is malformed.not_confirmable- this payment was not created for embedded confirmation.- 429
rate_limited- too many confirm attempts.
post/api/v1/payments/{id}/refunds
Refund a captured or settled payment
Full or partial, from a captured, partially refunded or settled payment (a settled payment is refunded at the provider like any other; it moves to
PARTIALLY_REFUNDEDorREFUNDEDonce the provider confirms). The amount is clamped to the refundable balance, and concurrent full refunds cannot both succeed. Send anIdempotency-Key: it is REQUIRED for aps_live_key (a keyless live refund is refused 400idempotency_key_required) and optional in sandbox. On this endpoint the key also names the refund itself, so a request repeated under the same key is answered from that refund and never refunds twice - which is exactly why a live refund must carry one: without it, a network retry is a second refund. 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. - 200
- Refunded: the provider confirmed it.
- 401
- Missing, malformed, revoked, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 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.simulated_payment- a live payment a simulation answered (no provider holds it, no money moved): on a production deployment a simulation never answers an operation on a live payment, so nothing was sent and nothing changed.- 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.- 503
provider_unavailable- the payment is held by a real provider that this deployment cannot reach (the connection's credentials are missing, or live provider access is off), so the refund was NOT sent and nothing changed. Payshen never lets a simulation answer for a real provider's payment. Retry once the connection reaches its provider; the Idempotency-Key is released, so a new attempt is safe.
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 (
CANCELLED). OrPENDINGwhen the provider accepted the void and reports its result later:payment.voidedfollows when the provider confirms it. - 400
idempotency_key_required- aps_live_key sent no Idempotency-Key (required in production, optional in sandbox);invalid_idempotency_key- the Idempotency-Key is not 8-200 printable ASCII characters.- 401
- Missing, malformed, revoked, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 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.simulated_payment- a live payment a simulation answered (no provider holds it, no money moved): on a production deployment a simulation never answers an operation on a live payment, so nothing was sent and nothing changed.- 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.- 503
provider_unavailable- the payment is held by a real provider that this deployment cannot reach (the connection's credentials are missing, or live provider access is off), so the void was NOT sent and nothing changed. Payshen never lets a simulation answer for a real provider's payment. Retry once the connection reaches its provider; the Idempotency-Key is released, so a new attempt is safe.
Providers
get/api/v1/provider-webhooks/{connectionId}no auth
Inbound provider payment callback (GET)
The same callback, for rails that send it as a GET with every parameter in the query string (Verotel's postbacks). Handled exactly as the POST, with an empty body; the query string reaches the connection's adapter, which verifies it. Called by the provider, not by you.
Field Type Notes connectionId* string pathThe provider connection the endpoint belongs to. - 200
- Accepted and stored, answered as the rail expects (Verotel: plain
OK). - 401
- The signature does not hold. An unknown connection, and a rail that takes no callbacks, answer identically by design.
- 413
- Query string over 64 KB.
- 429
- Rate limited.
post/api/v1/provider-webhooks/{connectionId}no auth
Inbound provider payment callback
Called by the payment provider holding one of your provider connections, not by you. The provider signs it with the secret issued for that connection and the connection's adapter verifies it; the event is stored before it is interpreted, and it can only move the payment that connection holds. A redelivery of the same event id is answered 200 and changes nothing. Documented so integrators understand the inbound path, not so they call it.
Field Type Notes connectionId* string pathThe provider connection the endpoint belongs to. - 200
- Accepted and stored.
resultsays what it did:apply,ignoredorreview;duplicatemarks a delivery already held. A rail that expects its own acknowledgement gets exactly that instead of JSON (VerotelOK, Vendo's XML, emerchantpay's notification echo, Praxis's signed reply), for a repeat as well. - 401
- The signature does not hold. An unknown connection, and a rail that takes no callbacks, answer identically by design.
- 413
- Body or query string over 64 KB.
- 429
- Rate limited.
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.
Suggestions
get/api/v1/suggestions
List suggestions
The routing engine's suggestions for the key's environment, newest first: the same ones the dashboard shows as cards. A suggestion carries a monthly money figure (
value) only when the lower end of its 95% interval is above zero; otherwisevalueis null andvalue_reasonsays why (aworth_testingsuggestion is one only a routing test can measure). Its evidence is frozen when it is made, with a hash, and once acceptedrealizedsays what it actually did. Recomputed at most hourly: listen forsuggestion.createdrather than polling. Payshen never holds or moves money; nothing here is a balance. API keys carry no narrower scope: any key of the workspace may read and decide suggestions in its own environment. Rate limited to 30 calls a minute.Field Type Notes state string queryComma-separated states to return: open,snoozed,accepted,rejected,superseded,expired. A snooze that ran out counts asopen.limit integer ≥ 1 query before string queryCursor: the next_cursorof the previous page.- 200
- A page of suggestions.
- 401
- Missing, malformed, revoked, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 422
validation_error(an unknown state, a limit out of range, or a cursor that is not an id).- 429
- Rate limited. Retry after the interval in the Retry-After header.
get/api/v1/suggestions/{id}
Retrieve a suggestion
One suggestion of this workspace in the key's environment. Rate limited to 60 calls a minute.
Field Type Notes id* string pathThe suggestion's id.- 200
- The suggestion.
- 401
- Missing, malformed, revoked, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 404
not_found- no such suggestion in this workspace and the key's environment.- 429
- Rate limited. Retry after the interval in the Retry-After header.
post/api/v1/suggestions/{id}/accept
Accept a suggestion
Applies it the safe way, exactly as the dashboard's Accept does; live traffic is never repointed in one call. A routing suggestion starts a routing test that sends 10% of the segment to the suggested connection for up to 14 days on the published rule that carries it (the engine promotes or stops the test on its own), or, when no published rule carries the segment, writes a draft rule a person publishes. A pricing suggestion writes the learned rates as draft fee rules to confirm.
result.outcomesays which. Requires anIdempotency-Key, in sandbox too: a lost answer retried under the same key replays it and never starts a second test. Rate limited to 30 calls a minute.Field Type Notes id* string pathThe suggestion's id.Idempotency-Key* string headerRequired. Reuse it when you retry. - 200
- Accepted.
datais the suggestion, nowaccepted;resultwhat was applied. - 400
idempotency_key_required- no Idempotency-Key was sent (required on this call with any key);invalid_idempotency_key- it is not 8-200 printable ASCII characters.- 401
- Missing, malformed, revoked, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 404
not_found- no such suggestion in this workspace and the key's environment.- 409
suggestion_not_open- it was already accepted, rejected, superseded or expired;suggestion_connection_gone- a connection it names is no longer active;nothing_to_apply- its evidence no longer yields a change to apply;idempotency_in_progress- a request with this Idempotency-Key is still being processed.- 422
idempotency_key_reuse- this Idempotency-Key was used with a different request.- 429
- Rate limited. Retry after the interval in the Retry-After header.
post/api/v1/suggestions/{id}/reject
Reject a suggestion
Declines it, with an optional reason kept with it. It does not come back unless the evidence behind it materially changes (then it is a new suggestion). Rate limited to 30 calls a minute.
Field Type Notes id* string pathThe suggestion's id.Idempotency-Key string headerOptional. A retry under the same key replays the first answer. reason string Why, in your words. - 200
- Rejected.
datais the suggestion, nowrejected. - 400
invalid_json- the body is not JSON;invalid_idempotency_key- the Idempotency-Key is not 8-200 printable ASCII characters.- 401
- Missing, malformed, revoked, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 404
not_found- no such suggestion in this workspace and the key's environment.- 409
suggestion_not_open- it was already accepted, rejected, superseded or expired (read it again for its state);idempotency_in_progress- a request with this Idempotency-Key is still being processed.- 413
payload_too_large.- 422
validation_error- a field other than reason, or a reason over 500 characters;idempotency_key_reuse- this Idempotency-Key was used with a different body.- 429
- Rate limited. Retry after the interval in the Retry-After header.
post/api/v1/suggestions/{id}/snooze
Snooze a suggestion
Hides it for 7 days, as the dashboard's Ignore does. It comes back as
openon its own if the evidence still holds, and isexpiredif it no longer does. Rate limited to 30 calls a minute.Field Type Notes id* string pathThe suggestion's id.Idempotency-Key string headerOptional. A retry under the same key replays the first answer. - 200
- Snoozed.
datais the suggestion, nowsnoozed, withsnoozed_until. - 400
invalid_idempotency_key- the Idempotency-Key is not 8-200 printable ASCII characters.- 401
- Missing, malformed, revoked, expired or unknown API key. An expired key's message gives the date it expired.
- 403
tenant_suspended: the workspace is suspended.account_not_approved: a live key, or a live payment, before Payshen has reviewed and approved the account; test keys are never refused for it.insufficient_scope: the key is restricted and lacks the scope this operation names under security.- 404
not_found- no such suggestion in this workspace and the key's environment.- 409
suggestion_not_open- it was already accepted, rejected, superseded or expired (read it again for its state);idempotency_in_progress- a request with this Idempotency-Key is still being processed.- 422
idempotency_key_reuse- this Idempotency-Key was used with a different body.- 429
- Rate limited. Retry after the interval in the Retry-After header.
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 | A POST with a production (ps_live_) key sent no Idempotency-Key. Sandbox (ps_test_) keys may omit it, except to accept a suggestion, which always needs one. |
| 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, expired (the message gives the date) or unknown. |
| tenant_suspended | 403 | The workspace is suspended. |
| account_not_approved | 403 | A live key or live payment before the account is approved by Payshen. Test keys are never refused for it; send the account for review on Get live. |
| insufficient_scope | 403 | The API key is restricted, and the call needs a scope it does not have (the scopes are listed under Authentication in the developer guide). Give the key that scope, or use another key. |
| not_a_partner | 403 | A partner endpoint was called by a workspace that is not a partner. |
| live_key_required | 403 | Listing or provisioning merchants was attempted with a test key. |
| not_found | 404 | No such payment or suggestion. Also returned for one in another workspace or environment. |
| idempotency_in_progress | 409 | A request with the same Idempotency-Key is still being processed. Retry shortly, with the same key. |
| routing_failed | 409 | No connection can take this payment. With `debug: true` the answer lists why each connection was excluded. |
| not_capturable | 409 | Capture asked for a payment that is not AUTHORIZED: already captured or voided, or a capture or void is still in flight. |
| capture_conflict | 409 | Another capture or void took the payment first. |
| not_voidable | 409 | Void asked for a payment that is not AUTHORIZED. |
| void_conflict | 409 | Another capture or void took the payment first. |
| not_refundable | 409 | Refund asked for a payment with nothing captured left to refund. |
| refund_conflict | 409 | Another refund on the same payment is being recorded. Retry shortly. |
| refund_pending | 409 | The provider accepted the refund and has not confirmed it yet. It is not refunded yet: do not send it again. The same Idempotency-Key answers the final result once the provider confirms, and you get `payment.refunded` or `payment.refund_failed`. |
| no_provider_ref | 409 | The payment has no provider reference to act on (it never reached a provider). |
| simulated_payment | 409 | A live payment that only a simulation answered: no provider holds it, so nothing was sent. |
| suggestion_not_open | 409 | The suggestion was already accepted, rejected, superseded by stronger evidence, or expired. Read it again for its state. |
| suggestion_connection_gone | 409 | A connection the suggestion names is no longer active, so accepting it cannot apply anything. |
| nothing_to_apply | 409 | The evidence behind the suggestion no longer yields a change to apply. Read it again. |
| already_invited | 409 | That owner already has a pending invitation. Safe to treat as done. |
| session_expired | 410 | The checkout session behind a browser call has expired. Create a new payment. |
| payload_too_large | 413 | The body exceeds 16 KB. |
| validation_error | 422 | A field is missing or invalid. The message says which. |
| invalid_amount | 422 | The amount is not a positive integer in minor units, or is more than can be captured or refunded. |
| idempotency_key_reuse | 422 | The Idempotency-Key was used before with a different body. |
| return_url_required | 422 | `ui: embedded` needs a `return_url`, for 3-D Secure redirects and the fallback page. |
| return_url_not_allowed | 422 | With a live key, `return_url` or `cancel_url` points at a host that is not on your allowed return hosts (Developers in the dashboard). Off unless you turn it on. |
| no_eligible_connection | 422 | No active connection can take an embedded payment like this one. |
| no_eligible_method | 422 | No active connection can take this hosted payment by any method you allowed. The message lists why per method. |
| pan_not_accepted | 422 | A field holds what looks like a card number (on create: reference, description, metadata or any other field), or raw card data was sent to a browser call. Payshen never accepts card data; only a provider-issued token is. |
| not_confirmable | 422 | The payment cannot be confirmed or completed from the browser in its current state. |
| country_blocked | 422 | Partner provisioning: Payshen does not onboard merchants registered in the `country` you sent. |
| 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. |
| state_unconfirmed | 500 | The provider confirmed the operation and recording it failed. Do not repeat it: Payshen reconciles it from the provider's record. |
| unsupported | 501 | This provider connection cannot do that operation. |
| capture_result_unknown | 502 | The provider did not answer the capture and may have captured: the payment is UNKNOWN. Do not retry; the same Idempotency-Key replays this answer, and Payshen settles the payment from the provider's record. |
| void_result_unknown | 502 | The provider did not answer the void and may have voided: the payment is UNKNOWN. Do not retry; Payshen settles the payment from the provider's record. |
| refund_result_unknown | 502 | The provider did not answer the refund and may have refunded: the amount stays in refund_pending_amount. Do not retry, with any key; Payshen settles it and the payment shows the result. |
| psp_error | 502 | The provider refused or failed the operation. Nothing changed; the message carries the provider's reason. |
| provider_unavailable | 503 | The provider could not be reached. Nothing changed; retry later with the same Idempotency-Key. Confirming an embedded payment answers it with 409. |
A 4xx is an answer: retrying it unchanged gets the same answer. The exceptions say so themselves: idempotency_in_progress, refund_conflict and rate_limited are worth a retry shortly, with the same Idempotency-Key. A 5xx or a timeout is worth retrying with the same key too, except payment_result_unknown, capture_result_unknown, void_result_unknown, refund_result_unknown and state_unconfirmed, where the provider may already have acted: read the payment instead.
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: choose Every event or tick the types under Developers → Webhooks, when you add the endpoint or later with Change events on its page. Every event includes types added to this list later; an endpoint added before subscriptions existed receives every event. Sandbox endpoints receive only sandbox events, production endpoints only production events. Each endpoint's page lists the last 30 days of deliveries with what your endpoint answered, and can resend an event that was given up.
| Event | Sent when |
|---|---|
| payment.created | A hosted payment was created and is waiting for the customer.Also in data: amount, currency |
| payment.approved | The payment was approved and captured in one step.Also in data: payment_status, amount, currency, country, method, provider, provider_ref, fee_amount, expected_fee, retried, recovered |
| payment.authorized | The payment was approved with capture_method manual, and is held for capture.Also in data: payment_status, amount, currency, country, method, provider, provider_ref, fee_amount, expected_fee, retried, recovered |
| payment.captured | An authorization was captured, in full or in part. Also the outcome of an embedded card payment whose customer completed 3-D Secure (authenticated says whether the issuer authenticated them). 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.Also in data: captured_amount, fee_amount, expected_fee, corrected, previous_status |
| payment.declined | The payment was declined. After a 3-D Secure challenge the customer failed, decline_code is the provider's (such as payment_intent_authentication_failure).Also in data: payment_status, decline_reason, amount, currency, provider, retried |
| 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. A later event settles it.Also in data: payment_status, amount, currency, provider, retried |
| 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.Also in data: payment_status, decline_reason, amount, currency, provider |
| payment.voided | An authorization was voided. When the provider only accepted the void, this is sent once it confirms. |
| payment.cancelled | Nothing was charged and the payment ended. reason says why: customer_abandoned (cancelled on the hosted page), session_expired (the hosted page expired unused) or authentication_abandoned (a 3-D Secure challenge was not finished within an hour and Payshen cancelled it at the provider).Also in data: reason |
| payment.refunded | A refund went through, in full or in part. A refund the provider only accepted is sent when the provider confirms it, which can take minutes (Adyen) to hours.Also in data: refund_amount, refund_id, refunded_total, refund_pending_amount |
| 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.Also in data: refund_amount, refund_id, refunded_total, refund_pending_amount |
| dispute.opened | The provider opened a dispute (a chargeback or an inquiry) on a payment. respond_by is the evidence deadline: missing it loses the money by default. |
| dispute.updated | An open dispute changed status at the provider, for example from EVIDENCE_REQUIRED to UNDER_REVIEW. |
| dispute.closed | A dispute was decided: WON, LOST, or EXPIRED (lost by default). A lost dispute moves the payment to CHARGEBACK, shown in payment_status. |
| suggestion.created | The routing engine has a new suggestion the evidence supports (checked at most hourly). Its value is a monthly figure only when the lower end of its 95% interval is above zero; otherwise value is null and value_reason says why. Nothing changes until someone accepts it. |
| suggestion.updated | A suggestion changed: it was accepted, rejected or snoozed (in the dashboard or by API), replaced by stronger evidence (superseded), or dropped because the evidence no longer supports it (expired); or what an accepted one realized moved to measured, or its verdict changed. changed says which (state or realized), and previous_state what it was.Also in data: changed, previous_state |
| webhook.test | Someone pressed Send test event in the dashboard. It never describes a real payment. |
What every event carries
Every payment.* event's data has id, payment_id, reference, status, environment. There id is the payment's id. On payment.approved, .authorized, .declined, .failed and .unknown, status is the outcome in lowercase (approved, declined, ...), kept for compatibility, and payment_status is the payment's status in UPPERCASE: branch on payment_status there.
Every dispute.* event's data has id, dispute_id, payment_id, payment_reference, payment_status, status, provider_status, provider_dispute_id, reason, provider_reason_code, amount, currency, fee, respond_by, opened_at, resolved_at, environment. There id and status are the dispute's; the payment is payment_id.
Every suggestion.* event's data is the suggestion as GET /api/v1/suggestions/{id} answers it: id, key, kind, state, environment, claim, title, value, value_reason, change, evidence, applied, realized, realized_at, snoozed_until, created_at, state_changed_at, dashboard_path. value is a monthly figure only when the lower end of its 95% interval is above zero; otherwise it is null and value_reason says why. These events carry no payment.
Retries
Answer with any 2xx within 5 seconds. Anything else is sent again with the same event id, 1 min, 5 min, 15 min, 30 min, 1 h, 2 h, 4 h, 8 h, 12 h apart: ten attempts over about 28 hours. Then the event is given up, and you can resend it from the endpoint's page once your endpoint is fixed. 400, 410, 422 are the exceptions: they are read as your final answer about that event and are not retried. A 401, 403 or 404 is retried, because it is usually a deploy or a secret rotation in progress.
Deduplicate on event.id
Because deliveries are retried, the same event can arrive twice, and events are not guaranteed to arrive in order. Record each id and skip what you have already processed. claimWebhookEvent does this in one call.