Skip to content

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.

    FieldTypeNotes
    typestringqueryExact event type, e.g. payment.captured.
    beforestringqueryCursor: the last id from the previous page.
    limitinteger ≥ 1query
    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

    FieldTypeNotes
    id*stringpathThe payment id returned as payment_id on 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.

    FieldTypeNotes
    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. country is checked against Payshen's country gate: a country Payshen does not onboard is refused with country_blocked; a country under review is provisioned, and the merchant's going live and provider applications wait until Payshen clears the workspace.

    FieldTypeNotes
    name*string
    owner_email*string
    plan"core" | "scale" | "enterprise"
    countrystringISO 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_review is approved when 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, or live_key_required when called with a test key.
    409
    already_invited.
    422
    validation_error, or country_blocked when 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.

    FieldTypeNotes
    statusstringqueryCase-insensitive.
    beforestringqueryCursor: the last id from the previous page.
    limitinteger ≥ 1query
    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. With return_url the response is a redirect instead of an outcome.

    FieldTypeNotes
    Idempotency-Keystringheader8-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 400 idempotency_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, else UNKNOWN. The key is kept 24 hours after its answer, and for as long as the provider attempt has no answer or its payment is UNKNOWN; after that the same key is a new request.
    amount*integer ≥ 50Minor units of currency. 5000 is EUR 50.00, and JPY 5,000.
    currency*stringISO 4217.
    country*stringISO 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 what method alone has always done. When no connection can take any of them the create is refused 422 no_eligible_method, so a checkout that could offer the customer nothing is never opened.
    card_brandstring
    referencestringYour own identifier. Echoed back and in webhooks.
    initiator"cit" | "mit"A label for routing and fee modelling: mit marks 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_urlstringPresent to use the hosted redirect flow. Must be https with a live key (ps_live_); a test key may use http.
    cancel_urlstringMust be https with a live key (ps_live_); a test key may use http.
    debugbooleanWhen true, routing.excluded lists every connection removed before routing, with a code and a reason, and a routing_failed error 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) returns next_action.REDIRECT to a hosted page. embedded routes now and returns a collector (the routed provider's browser fields or a native action) plus a client_secret, for the Checkout SDK to mount in your page; the card goes browser -> provider and never through Payshen, and you complete it with POST /v1/payments/{id}/confirm. Requires return_url. Additive and opt-in.
    customerobjectOptional; 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_key or invalid_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, or routing_failed when no connection can take it. With debug: true a routing_failed error also carries error.excluded, the same list as routing.excluded.
    413
    payload_too_large - the body exceeds 16 KB.
    422
    validation_error, or idempotency_key_reuse for 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 without return_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 carries request_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 (often UNKNOWN). A daily job records such a payment even if nobody retries.
  • get/api/v1/payments/{id}

    Retrieve a payment

    FieldTypeNotes
    id*stringpathThe payment id returned as payment_id on 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: manual can be captured.

    FieldTypeNotes
    id*stringpathThe payment id returned as payment_id on creation.
    amountinteger ≥ 1Minor units. Omit to capture the full authorization.
    200
    Captured (CAPTURED). Or PENDING when the provider accepted the capture and reports its result later (Adyen, Solidgate): nothing is captured yet, and payment.captured follows when the provider confirms it.
    400
    idempotency_key_required - a ps_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 not AUTHORIZED (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_error or invalid_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 is AUTHORIZED again; 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 is UNKNOWN. 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 session client_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_reason e.g. payment_intent_authentication_failure) or CANCELLED - after checking its amount and currency against the payment. Still REQUIRES_ACTION with a fresh next_action while 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, by payment.captured, payment.authorized, payment.declined or payment.cancelled.

    FieldTypeNotes
    id*stringpathThe payment id returned as payment_id on creation.
    client_secret*stringThe 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_secret from an embedded create response (ui: "embedded"), NOT the ps_live_ API key - which must never ship to the browser. It accepts ONLY a provider-issued token via payment_method; raw card data (PAN, CVV, expiry) is refused with pan_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 is REQUIRES_ACTION with next_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 call POST /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 ends CANCELLED (payment.cancelled, reason authentication_abandoned).

    FieldTypeNotes
    id*stringpathThe payment id returned as payment_id on creation.
    client_secret*stringThe session client secret from the embedded create response. Bound to this payment; short-lived; not the API key.
    payment_method*stringThe 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_ACTION with next_action when 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_REFUNDED or REFUNDED once the provider confirms). The amount is clamped to the refundable balance, and concurrent full refunds cannot both succeed. Send an Idempotency-Key: it is REQUIRED for a ps_live_ key (a keyless live refund is refused 400 idempotency_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 in refund_pending_amount, not in refunded_amount, and is settled from the provider's own record: a refund still pending is never answered as refunded.

    FieldTypeNotes
    id*stringpathThe payment id returned as payment_id on creation.
    Idempotency-Keystringheader8-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_pending while 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.
    amountinteger ≥ 1Minor 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 in refund_pending_amount. Do not retry with a new key; the same Idempotency-Key answers its final result once it is settled. Also not_refundable and refund_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_error or invalid_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 in refund_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.

    FieldTypeNotes
    id*stringpathThe payment id returned as payment_id on creation.
    200
    Voided (CANCELLED). Or PENDING when the provider accepted the void and reports its result later: payment.voided follows when the provider confirms it.
    400
    idempotency_key_required - a ps_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 not AUTHORIZED (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 is AUTHORIZED again; 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 is UNKNOWN. 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.

    FieldTypeNotes
    connectionId*stringpathThe 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.

    FieldTypeNotes
    connectionId*stringpathThe provider connection the endpoint belongs to.
    200
    Accepted and stored. result says what it did: apply, ignored or review; duplicate marks a delivery already held. A rail that expects its own acknowledgement gets exactly that instead of JSON (Verotel OK, 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.

    FieldTypeNotes
    pspId*stringpath
    X-Payshen-Signature*stringheadert=<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; otherwise value is null and value_reason says why (a worth_testing suggestion is one only a routing test can measure). Its evidence is frozen when it is made, with a hash, and once accepted realized says what it actually did. Recomputed at most hourly: listen for suggestion.created rather 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.

    FieldTypeNotes
    statestringqueryComma-separated states to return: open, snoozed, accepted, rejected, superseded, expired. A snooze that ran out counts as open.
    limitinteger ≥ 1query
    beforestringqueryCursor: the next_cursor of 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.

    FieldTypeNotes
    id*stringpathThe 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.outcome says which. Requires an Idempotency-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.

    FieldTypeNotes
    id*stringpathThe suggestion's id.
    Idempotency-Key*stringheaderRequired. Reuse it when you retry.
    200
    Accepted. data is the suggestion, now accepted; result what 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.

    FieldTypeNotes
    id*stringpathThe suggestion's id.
    Idempotency-KeystringheaderOptional. A retry under the same key replays the first answer.
    reasonstringWhy, in your words.
    200
    Rejected. data is the suggestion, now rejected.
    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 open on its own if the evidence still holds, and is expired if it no longer does. Rate limited to 30 calls a minute.

    FieldTypeNotes
    id*stringpathThe suggestion's id.
    Idempotency-KeystringheaderOptional. A retry under the same key replays the first answer.
    200
    Snoozed. data is the suggestion, now snoozed, with snoozed_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.

StatusMeaning
CREATEDThe payment exists and nothing has been attempted at a provider yet.
PENDINGThe provider has it and is processing. In flight, not an outcome.
REQUIRES_ACTIONWaiting on the customer. Send them to next_action.url.
AUTHORIZEDApproved and held, not yet captured. Capture it or void it.
CAPTUREDApproved and captured at the provider.
DECLINEDThe provider refused it. decline_category carries the normalized reason.
FAILEDIt could not be completed, and will not be. The payment's timeline records why.
UNKNOWNThe provider may have executed the operation and we could not tell (a timeout after the request was sent, a connection lost mid-answer).
CANCELLEDEnded before any money was taken: an authorization voided, or the customer cancelled on the hosted page.
REFUNDEDThe whole captured amount has been refunded.
PARTIALLY_REFUNDEDPart of the captured amount has been refunded; the rest can still be.
CHARGEBACKA chargeback was recorded against it.
SETTLEDThe 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.

CodeHTTPMeaning
invalid_json400The body is not valid JSON.
idempotency_key_required400A 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_key400The Idempotency-Key header is not 8 to 200 printable ASCII characters.
unauthorized401The API key is missing, malformed, revoked, expired (the message gives the date) or unknown.
tenant_suspended403The workspace is suspended.
account_not_approved403A 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_scope403The 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_partner403A partner endpoint was called by a workspace that is not a partner.
live_key_required403Listing or provisioning merchants was attempted with a test key.
not_found404No such payment or suggestion. Also returned for one in another workspace or environment.
idempotency_in_progress409A request with the same Idempotency-Key is still being processed. Retry shortly, with the same key.
routing_failed409No connection can take this payment. With `debug: true` the answer lists why each connection was excluded.
not_capturable409Capture asked for a payment that is not AUTHORIZED: already captured or voided, or a capture or void is still in flight.
capture_conflict409Another capture or void took the payment first.
not_voidable409Void asked for a payment that is not AUTHORIZED.
void_conflict409Another capture or void took the payment first.
not_refundable409Refund asked for a payment with nothing captured left to refund.
refund_conflict409Another refund on the same payment is being recorded. Retry shortly.
refund_pending409The 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_ref409The payment has no provider reference to act on (it never reached a provider).
simulated_payment409A live payment that only a simulation answered: no provider holds it, so nothing was sent.
suggestion_not_open409The suggestion was already accepted, rejected, superseded by stronger evidence, or expired. Read it again for its state.
suggestion_connection_gone409A connection the suggestion names is no longer active, so accepting it cannot apply anything.
nothing_to_apply409The evidence behind the suggestion no longer yields a change to apply. Read it again.
already_invited409That owner already has a pending invitation. Safe to treat as done.
session_expired410The checkout session behind a browser call has expired. Create a new payment.
payload_too_large413The body exceeds 16 KB.
validation_error422A field is missing or invalid. The message says which.
invalid_amount422The amount is not a positive integer in minor units, or is more than can be captured or refunded.
idempotency_key_reuse422The Idempotency-Key was used before with a different body.
return_url_required422`ui: embedded` needs a `return_url`, for 3-D Secure redirects and the fallback page.
return_url_not_allowed422With 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_connection422No active connection can take an embedded payment like this one.
no_eligible_method422No active connection can take this hosted payment by any method you allowed. The message lists why per method.
pan_not_accepted422A 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_confirmable422The payment cannot be confirmed or completed from the browser in its current state.
country_blocked422Partner provisioning: Payshen does not onboard merchants registered in the `country` you sent.
rate_limited429Too many requests. Wait for the Retry-After interval.
merchant_limit429The partner's merchant allowance is used up.
internal_error500Something failed on our side. Safe to retry with the same Idempotency-Key.
payment_result_unknown500A 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_unconfirmed500The provider confirmed the operation and recording it failed. Do not repeat it: Payshen reconciles it from the provider's record.
unsupported501This provider connection cannot do that operation.
capture_result_unknown502The 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_unknown502The 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_unknown502The 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_error502The provider refused or failed the operation. Nothing changed; the message carries the provider's reason.
provider_unavailable503The 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.

EventSent when
payment.createdA hosted payment was created and is waiting for the customer.Also in data: amount, currency
payment.approvedThe 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.authorizedThe 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.capturedAn 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.declinedThe 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.unknownThe 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.failedNo 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.voidedAn authorization was voided. When the provider only accepted the void, this is sent once it confirms.
payment.cancelledNothing 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.refundedA 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_failedA 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.openedThe 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.updatedAn open dispute changed status at the provider, for example from EVIDENCE_REQUIRED to UNDER_REVIEW.
dispute.closedA dispute was decided: WON, LOST, or EXPIRED (lost by default). A lost dispute moves the payment to CHARGEBACK, shown in payment_status.
suggestion.createdThe 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.updatedA 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.testSomeone 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.