Skip to content

Developers Changelog

What changed, and when

Security fixes lead and say what the defect could actually do: deciding whether tonight is the night to upgrade needs the failure mode, not a reassurance. The v1 REST contract is frozen; anything breaking would ship as v2 alongside it.

API v1

API

Allowed return hosts: an opt-in list of where a live payment may send the customer back.

Security

  • Allowed return hosts

    Under Developers in the dashboard you can list the hosts a live payment's return_url and cancel_url may point at, exactly (shop.example.com) or with their subdomains (*.example.com). With a list set, a live create that points anywhere else is refused with 422 return_url_not_allowed, so a leaked live key cannot send your customers to a look-alike page. Off until you turn it on; test keys are never held to it.

WooCommerce plugin 1.1.0

SDK

Payshen for WooCommerce is in the WordPress.org plugin directory. Requires PHP 8.2.

Security

  • Payment page and payment ids

    The customer is sent to a payment page only over https (http on localhost aside). A payment id is checked before it goes into an API address, so an id such as .. is refused with invalid_payment_id and nothing is sent.

Changes

  • Install from WordPress.org

    Plugins → Add New, search for Payshen for WooCommerce, Install Now. Updates now arrive in WordPress like any other plugin's. The guide has the setup.

  • Connect with Payshen

    Sign in to Payshen and approve the store; its API keys and webhooks are set up for you. Disconnect revokes the keys in Payshen. Saved keys and webhook secrets are no longer written into the settings page.

API v1 · sdk 0.10.1, checkout 0.4.0, PHP 0.2.1, Python 0.1.3

API

Security hardening ahead of an independent penetration test.

Security

  • Connect with Payshen

    Approving a store now needs two-factor authentication, as creating keys in the console does; a code approved without it is refused at /api/connect/token with 403 access_denied. /api/connect/disconnect removes only the webhook endpoint on the calling store's own address.

  • Sign-in and limits

    Someone else asking for sign-in links to your address no longer locks you out; correct authenticator codes no longer count towards the 30-minute lock; per-address limits count an IPv6 client by its /64.

  • Inbound webhooks

    PayPal, MiFinity and unsigned Computop callbacks share the lower read-back budget of 60 a minute per connection; beyond it the provider retries and reconciliation checks the payment. /api/v1/psp-webhooks/{id} gives one answer for every refusal; the reason is in the provider's portal.

  • Client libraries

    The API key is no longer shown when a client is logged (TypeScript, PHP); an id that is not letters, digits, _ or - is refused before a request; an empty webhook secret is treated as no secret; @payshen/checkout refuses a baseUrl starting with //.

API v1 · checkout 0.4.0

API

API keys can expire; unused accounts are disabled after 90 days.

Changes

  • API key expiry

    A new key can be given an expiry of 90, 180 or 365 days. An expired key is answered 401 unauthorized with the date it expired. Existing keys have none and keep working until revoked.

  • Accounts

    A console account with no sign-in and no activity for 90 days is disabled automatically; support re-enables it. API keys are not accounts and are not affected.

  • Checkout

    @payshen/checkout sends the customer only to https addresses (http on localhost aside) and requires an https baseUrl, from 0.4.0 (24 October 2026).

API v1

API

A card number in any field is refused, and changes in the console need two-factor.

Security

  • No card data, anywhere

    POST /v1/payments now answers 422 pan_not_accepted when any field (a reference, metadata, a description) holds a card number, as /confirm and /complete already did. Provider webhooks, dispute payloads and settlement files are stored without CVV, PIN, track data or expiry, and a full card number in them is cut to its last four.

  • Two-factor to change things

    In the console, anyone whose role can change providers, routing, payments, API keys, webhooks, people or settings needs two-factor authentication to do so; reading stays open. Ten wrong codes lock the account for 30 minutes, and a session left alone for 15 minutes asks for a code again. API keys are not affected. Refused sign-ins, codes and API keys are now on the audit log.

API v1 · engine 3.5.0

API

Smart routing reacts to a provider that suddenly declines more within a few dozen payments.

Changes

  • Routing

    Beside each provider's longer record, Smart routing now keeps a short one: about its last 50 results for that kind of payment. When the two clearly disagree, the provider is ranked on its recent results until the record catches up. In our simulation a provider dropping from 93% to 50% approval lost most of its traffic after about 350 payments before; now it takes a few dozen. The payment's routing shows "approving less lately" (or more) next to such a provider. Nothing changes for rules that do not use Smart routing.

API v1 · engine 3.4.0

API

Smart routing finds a good new provider even after an unlucky start.

Changes

  • Routing

    New providers share a small trial budget (about 1 payment in 10). Before, the provider picked for a trial still had to outrank the proven ones, which it rarely did, so a new provider that declined one of its first payments could stay unused however well it approves. Now the trial payment is tried there first, and the budget goes to the provider with the fewest results, so each one gets its turn. A poor provider costs at most 30 first tries before the evidence ranks it down, and a trial it soft-declines still moves on to your next provider.

PHP SDK 0.2.0 · WooCommerce plugin 1.1.0

SDK

PHP2Shell: Payshen is not affected; update the PHP on your own servers.

Security

  • PHP's critical security release (CVE-2026-109618, CVE-2026-109619)

    PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12 fix two flaws in PHP itself that let anyone run code on a server handling web requests. No Payshen system runs PHP, and none was affected. The PHP SDK and the WooCommerce plugin contain no affected code, but run on your PHP: update it. The notice.

Changes

  • PHP 8.2 or newer

    payshen/payshen-php 0.2.0 and the WooCommerce plugin 1.1.0 require PHP 8.2 or newer and are tested on 8.2 to 8.5. PHP 8.1 is end of life with no official fix; on it Composer keeps installing 0.1.2. The plugin's settings page warns when the server's PHP may be missing the fix. Your code does not change.

API v1

API

Smart routing learns in Test mode too, kept apart from live.

Changes

  • Routing

    Test payments now teach Smart routing in Test mode: approval, latency and fees are learned from your test traffic, the simulator's answers included, so you can watch routing learn before you go live. Live routing still learns only from live payments a real provider answered, and never reads what Test learned. An outcome you pick on the test payment page teaches nothing. Test payments may now be routed differently as Test mode learns.

PHP SDK 0.1.2 · Python SDK 0.1.2

SDK

Security fixes in webhook verification; upgrade.

Security

  • An empty webhook secret never verifies

    Before, a secret read from an unset environment variable (an empty string, or false in PHP), or an empty old secret in a rotation list, let anyone sign a forged event with an empty key. Both clients now ignore an empty or non-string secret and refuse with missing_secret when none is left. In Python, a hostile X-Payshen-Signature header (a non-ASCII v1, an over-long t) is now malformed_signature instead of an exception your handler did not catch.

Changes

  • Requests

    base_url must be https (http for localhost only). PHP checks an idempotency_key before sending it (8 to 200 printable ASCII characters, the API's own rule), so a line break cannot add a header, and limits curl to HTTP(S).

API v1 · SDK 0.10.0

API

Live return URLs must be https, and the SDK refuses redirects.

Changes

  • https return and cancel URLs for live payments

    With a live key, return_url and cancel_url on POST /v1/payments must be https; an http address is answered 422 validation_error naming the field. The customer comes back with the payment id, reference and status in the address, which must not travel in clear. Test keys may still use http, for a store on a laptop.

  • SDK: redirects refused, https base URL only

    The API never redirects, so @payshen/sdk no longer follows one: it throws unexpected_redirect, which means baseUrl is not the Payshen API. A base URL other than https (http for localhost only) is refused when the client is built.

API v1

API

Plans: unlimited supported providers from Scale up, brand limits, and Settings, Billing shows what your plan includes.

Changes

  • Providers and brands per plan

    Core includes six live providers and one brand; Scale unlimited supported providers and three brands; Enterprise and Enterprise + White Label unlimited supported providers and ten brands. Several accounts with one provider count as one, and test connections never count. A provider or brand past what a new contract includes is not added, with the reason; nothing already live is ever switched off. Existing contracts keep their terms.

API v1

API

Live payments start after a one-time account review; Settings, Billing shows what Payshen charges.

Changes

  • Account review

    A new workspace sends its business details (legal name, country of registration, website) for review from Get live. Until the account is approved by Payshen, live keys and live payments are refused with 403 account_not_approved; test keys never are. Workspaces created before today are already approved, and so are merchants an approved partner provisions.

  • What Payshen charges

    Settings, Billing now shows the price per production payment, this month's usage so far, and the invoices Payshen issued with what is still open. A payment is counted once whichever way it ends, including hosted payments finished after 3-D Secure or on a provider's page, which were not counted before. Partner merchants of an approved partner are billed by the partner, and the page says so.

API v1

API

Stripe and Checkout.com webhooks set up by Payshen; Checkout.com payouts read automatically; Revolut refunds confirmed at once.

Changes

  • Stripe webhook endpoint

    A Stripe connection no longer asks for a signing secret. When it is tested, Payshen creates the webhook endpoint in your Stripe account with your restricted key (it needs write on Webhook Endpoints) and keeps its secret. A secret you pasted before keeps working.

  • Checkout.com webhook

    A Checkout.com connection no longer asks for a webhook signature key. When it is tested, Payshen creates the webhook workflow in your Checkout.com account with a signature key of its own and keeps it. A key you entered before keeps working.

  • Checkout.com settlement sync

    Checkout.com connections now read the payout report Checkout.com makes for every payout: each capture, refund and chargeback with its interchange, scheme and processing fees, matched to the payment and grouped by payout. The secret key needs the Reports permission.

  • Revolut refunds

    A Revolut refund is confirmed as soon as Revolut completes it, from Revolut's own webhook, instead of at the next check.

API v1

API

Test and Live kept apart: providers, keys, webhooks and routing per mode. Nothing you send changes.

Changes

  • Providers

    Providers shows the connections of the mode you are in. A new connection is made in that mode. Go live on a test connection adds a separate live connection for the same provider, and the test one keeps working; a connection no longer changes mode when a key of the other mode is pasted.

  • Routing

    Test and Live have their own routing. Test payments follow your live rules until you set test routing up, and stay on test connections whenever you have one, so a live connection's figures never carry test traffic. Live routing can no longer send to a test connection.

  • API keys and webhooks

    The Developers page lists the keys and webhook endpoints of the mode you are in.

API v1

API

PayPal fees from your ledger, published list prices for 18 providers, and a check before some providers go live. Nothing you send changes.

Changes

  • Going live

    For some providers, Payshen switches live payments on after a short check. Testing a live connection to one of them confirms the key works and says the switch-on is in progress; you get an email when it is done. Test payments are never held, and live connections already running are not affected.

  • PayPal settlement sync

    PayPal connections now read the account's ledger (Transaction Search) like Stripe connections do: payments, refunds and chargebacks with PayPal's own fee, matched to the payment. PayPal's search runs about three hours behind, and the sync waits for it, so no line is skipped.

  • List prices

    The published standard prices of 18 providers (Stripe, Adyen, Mollie, PayPal, Revolut, Nexi, Worldline, Worldpay, Authorize.Net, Razorpay, Xendit, Mercado Pago, Flutterwave and others) are now in Payshen, read from each provider's own pricing page. Smart routing uses them only where you have stated no contract and no fee has been learned from your settlements.

API v1

API

PayPal and Revolut connect with the key alone: Payshen sets their webhook up. Nothing you send changes.

Changes

  • Provider webhooks

    When a PayPal or Revolut connection is tested, Payshen finds or creates the provider's webhook for the connection's callback URL with the key you pasted, and keeps its ID (PayPal) or signing secret (Revolut). There is no webhook to create in the provider's dashboard and nothing to paste. A webhook you created yourself is found and reused, never duplicated.

  • Callback secrets

    Twelve providers check their callbacks with a value among the credentials (Authorize.Net's Signature Key, Razorpay's webhook secret, Worldline's webhook key and others). Their Callbacks section no longer offers a separate signing-secret field they never read, and no longer calls one missing.

API v1

API

A provider's fee is recorded when the provider names it at capture. Nothing you send changes.

Changes

  • fee_amount

    When a provider states its fee on the capture itself (PayPal does, on every capture), Payshen now records it as the payment's fee_amount at once: on the payment, and in the payment.captured webhook when the capture was asked through the API or the dashboard. Before, it stayed 0 until the provider's settlement. A fee already recorded is never replaced, and expected_fee, Payshen's own estimate, is unchanged.

  • Payment page

    A payment paid on the provider's own page no longer shows "recovered by failover" when there was only one attempt, and its attempt reads as what the customer did (Customer paid, Declined or Closed) instead of "Waiting for customer".

API v1

API

Connect with Payshen: a WooCommerce store gets its keys without anyone copying them. Nothing you send changes.

Changes

  • WooCommerce

    Plugin 1.1.0 adds a Connect with Payshen button. The merchant signs in, picks a workspace and approves; the store trades a one-time code, with a PKCE verifier only it holds, for a test key and webhook (and live ones, when approved). The keys can only create, read, capture, release and refund payments. See Set up.

API v1

API

A declined order gets a second chance at another provider. Nothing you send changes.

Changes

  • Routing (engine 3.3.0)

    When a customer comes back within 24 hours to pay an order a provider just declined, Payshen sends the new payment to another provider first. The order is recognised by its reference, amount and currency, so send the same reference for each try at one order (the WooCommerce plugin does). Declines only the customer can fix, such as no funds or an expired card, change nothing, and fixed rules never reorder. The payment's routing record lists the providers that were moved in declinedEarlier.

  • Checks

    The audit trail's append-only guard from 14 October now installs on every database it runs on.

API v1

API

Security hardening from the review. Nothing you send changes.

Security

  • Secrets and records

    Provider credentials and webhook secrets are now sealed to the connection and workspace they belong to, so a copied value cannot be used anywhere else; older records are rewritten in the background. The audit trail is append-only in the database. A white-label custom domain counts only after a DNS TXT record proves you own it. A connection switched off, or withdrawn when its provider rejects the application, is no longer turned back on by a test.

Changes

  • Dashboard

    Settings lists your active sessions and can sign out all the others. Live API keys with more access than their maker's role now say so, so an owner can roll or restrict them.

API v1

API

Whole-platform security review. Two small API changes; nothing else you send changes.

Security

  • Fixed in the review

    Webhooks to an endpoint given by hostname failed to connect from our servers and were retried until given up; they are delivered again, and missed events can be read from the event feed or resent from the dashboard. Also fixed: workspace pages now check each role's read rights, a suspended workspace's members cannot open a new one, uploads are checked by their content, a chargeback is never matched to another merchant's payment by a shared order reference, and erasing a person also clears their address from dispute notes and audit details.

Changes

  • Live keys follow the maker's role

    A live API key can only do what the person who makes or rolls it may do in the dashboard: a Developer can no longer make a live key that refunds, captures or changes routing. Test keys are unchanged. Existing keys keep working.

  • Listing merchants needs a live key

    GET /api/v1/merchants now answers 403 live_key_required to a test key, as provisioning already did. A capture or refund body that is not a JSON object is refused with 422 instead of being read as the full amount.

API v1

API

Provider callbacks checked more strictly. Fix; nothing you send changes.

Security

  • Provider callbacks and amounts

    Affected: connections added on 14 October 2026 (Elavon, Fiserv, Klarna, Razorpay, N-Genius) and amounts in currencies whose decimals differ by provider. Fixed the same day, before any of these connections took a live payment: refund outcomes are always confirmed with the provider, callbacks are tied to the payment they name, a provider figure Payshen cannot represent exactly is sent to a review instead of being assumed, and callbacks that can only be read back are rate limited.

API v1

API

Stripe, Checkout.com and Paysafe receive amounts in their own currency units. Fix; nothing you send changes.

Changes

  • Currencies whose decimals differ by provider

    You send amounts in each currency's minor unit as Payshen documents it. For a few currencies (HUF, IDR, COP, PKR and others) a provider counts decimals differently. Stripe, Checkout.com and Paysafe now receive and report those amounts in their own units, as each provider documents, in both directions; an amount a provider cannot take exactly is refused before anything is sent.

API v1 · SDK 0.9.5

API

A refund names itself: its id on the refund's answer, on its errors and on payment.refunded. Additive.

Changes

  • Match a refund to its webhook

    POST /payments/{id}/refunds now answers refund: { id, amount, status } beside the payment's figures. A refund answered refund_pending, refund_result_unknown, psp_error or state_unconfirmed carries error.refund_id. payment.refunded carries the same refund_id, so a pending refund is matched to the webhook that settles it. In the TypeScript SDK: refund on the result and PayshenError.refundId.

WooCommerce plugin 1.0.0

SDK

Payshen for WooCommerce: take payments through Payshen in a WooCommerce store. New.

Changes

  • Upload, paste your key, take payments

    Customers pay on Payshen's secure page; orders update from signed webhooks and a 48-hour scheduled check; refunds (full and partial) from the order screen; manual capture and release; the Cart and Checkout blocks and the classic checkout; High-Performance Order Storage; test mode. WordPress 6.5+, WooCommerce 8.5+, PHP 8.1+. The guide has the setup.

API v1

API

Six more: D24, PayRetailers, PagSmile, Kushki, PPRO and Elavon. Additive.

Changes

  • Local payments in Latin America, Africa and Asia, and Elavon in Europe

    D24, PayRetailers, PagSmile and Kushki bring PIX, boleto, OXXO, SPEI, bank transfers, cash vouchers, wallets and cards through each provider's hosted page. PPRO brings local methods such as iDEAL, BLIK, Przelewy24, EPS, Swish, TWINT and UPI through one contract (local methods only; no cards). Elavon takes cards on the Elavon Payment Gateway's hosted page. Accounts with these providers are opened through their sales teams. D24 accepts calls only from addresses on its allowlist. PayRetailers refunds are made through PayRetailers itself.

API v1

API

Six more: Flutterwave, Network International (N-Genius), Global Payments, CyberSource, Skrill and Neteller. Additive.

Changes

  • Each on the provider's own hosted page

    Flutterwave Standard, the N-Genius Online payment page, the Global Payments hosted payment page, CyberSource Secure Acceptance, and Skrill and Neteller through the Paysafe Payments API. Card details and wallet logins never reach Payshen, and each provider pays the merchant's own account. Refunds can be partial. Skrill and Neteller are completed only while the payment still waits for its customer, so a payment Payshen has closed is never taken.

  • Stripe: card payments offered as a card

    Stripe no longer accepts a fixed list of payment methods on a new Checkout Session, so a card payment now filters Stripe's own choice to cards (allowed_payment_method_types). If an account refuses that too, the page opens with the account's methods and Stripe's reason is kept on the payment.

API v1

API

Nine more providers: PayPal, Klarna, Revolut Pay, Authorize.Net, Fiserv, Mercado Pago, Razorpay, Xendit and Tap. Additive.

Changes

  • Each on the provider's own hosted page

    A merchant connects their own account with each provider. The customer pays on that provider's page (PayPal, Klarna's Hosted Payment Page, Revolut's checkout, Authorize.Net Accept Hosted, Fiserv Checkout, Mercado Pago Checkout Pro, Razorpay Payment Links, Xendit Payment Sessions, Tap's hosted page), so card details never reach Payshen, and the provider pays out to the merchant's own account. Refunds can be partial everywhere. Manual capture is offered where the provider has it; Mercado Pago Checkout Pro and Razorpay Payment Links capture at once. Nothing changes in the API: route to them like any other connection.

API v1

API

A card payment is offered as a card on Stripe's page too. Behaviour fix; nothing you send changes.

Changes

  • Stripe Checkout shows the card, Apple Pay and Google Pay

    A payment created with method: "card" and sent to Stripe's hosted page is now offered there as a card, and the wallets that pay by card, as on Checkout.com and Adyen. If a Stripe account refuses a card-only page, the page opens with the account's methods as before, and Stripe's reason is shown on the payment so the account can be put right. To let the customer choose, send allowed_methods.

SDK 0.9.4

SDK

RoutingDecision.candidates_total. Additive; nothing you use changes.

Changes

  • candidates_total

    With many connections, routing.candidates lists the first ones tried and the best few by approval, by cost and by speed; candidates_total says how many were scored. Typed in RoutingDecision.

API v1

API

Smart routing ranks providers it knows nothing about by their fee, and tries new providers within one budget. Routing scales to hundreds of connections.

Changes

  • With nothing known, the fee decides

    Until a provider has enough results for a kind of payment, Smart routing used a random draw for it, so with several new providers "Lowest fees" chose almost at random (the cheapest of three won about 40% of payments). A provider without evidence is now ranked on what is known about it, and with nothing known the fee decides: the cheapest wins about nine payments in ten. Decisions record engine version 3.2.0 and replay under it.

  • New providers share one budget

    Trying new providers anyway, so they can earn evidence, is now about 1 payment in 10 in all, however many are new. It was 1 in 10 for each of them: with thirty new connections beside three proven ones, about a third of payments went to providers with no record.

  • Hundreds of connections

    Routing no longer opens every connection's stored key on every payment, reads learned results only for the connections in play, and keeps a bounded routing trace on each payment (routing.candidates_total says how many were ranked when fewer are listed). A rule can list any number of providers; it used to keep the first eight. The console finds and pages connections.

API v1

API

Routing rules match payments exactly as the console shows them. Behaviour fix; check rules that use "is none of" or an amount limit.

Changes

  • "Is none of" is honoured

    A rule condition such as "customer country is none of DE, FR, GB" was ignored by the router, so the rule matched payments from those countries too. The rule list, the tester and the rule's own words already excluded them; the router now does as well.

  • "Over" and "under" are strict

    "Amount over 500.00" matched a payment of exactly 500.00, and "under" its own limit likewise. They now mean strictly over and under, as written. "Between" still includes both ends.

  • Any country or currency in a rule

    The rule builder offered only the countries and currencies of past payments. Any ISO code can now be added, so a rule for a new market can exist before its first payment.

API v1

API

A card payment is offered as a card on Checkout.com's and Adyen's pages. Behaviour fix; nothing you send changes.

Changes

  • The method you asked for is the one the customer sees

    A payment created with method: "card" and sent to a provider's hosted page could be paid there with any method enabled on that account, such as pay by bank. That bypassed the routing rule that chose the connection for a card, at a fee the rule never weighed. Checkout.com's and Adyen's pages now offer the card, and the wallets that pay by card (Stripe's followed on 14 October). To let the customer choose, send allowed_methods: they pick on the Payshen page, and each method is routed on its own.

Python SDK 0.1.0

SDK

A Python client: pip install payshen. New; nothing existing changes.

Changes

  • payshen on PyPI

    For Python 3.9 and newer, on the standard library alone and typed: payments (create, get, list, capture, void, refund, timeline), events, insights, suggestions and partner merchants. An idempotency key on every POST, bounded retries with jitter that never retry an answer the API says not to, typed exceptions with code and request_id, and parse_webhook_event and claim_webhook_event for signed webhooks. The Python guide has every step; the source is public on GitHub (MIT).

PHP SDK 0.1.0

SDK

A PHP client: composer require payshen/payshen-php. New; nothing existing changes.

Changes

  • payshen/payshen-php on Packagist

    For PHP 8.1 and newer, with no dependencies beyond ext-curl and ext-json: payments (create, get, list, capture, void, refund, timeline), events, insights, suggestions and partner merchants. An idempotency key on every POST, bounded retries with jitter that never retry an answer the API says not to, typed exceptions with errorCode and requestId, and Webhook::parseEvent and Webhook::claimEvent for signed webhooks. The PHP guide has every step; the source is public on GitHub (MIT).

API v1

API

A missed callback settles itself. A decline on a provider's page says why. Examples in seven languages. Additive.

Changes

  • A callback that never arrives

    When a provider's callback is lost, Payshen asks the provider what became of the payment. That settled it on its own only for Stripe: for payabl. and Checkout.com the answer went to a person. The answer is now read through the adapter's own mapping, the one its callbacks are read by, with the same checks (a captured amount the provider does not state is never invented). A payment declined this way keeps its reason.

  • Why a hosted payment was declined

    A payment declined on a provider's own page reaches Payshen by the provider's callback. It became DECLINED without its reason. It now keeps it, as a decline answered at once does: decline_category and decline_code_raw on the payment, and decline_category and decline_code on its payment.declined webhook, as after a 3-D Secure challenge. Checkout.com's response codes are read into the same categories (20051 is insufficient_funds).

  • Examples in Python, PHP, Java, C# and Go

    The quickstart's create, capture, refund and webhook check are now in curl, TypeScript, Python, PHP, Java, C# and Go. Each one is run on every change: against a stand-in API that checks it sends the same request as the curl line and stops on a refusal, and against a real signature, a changed body and an old timestamp. A refund answered 409 refund_pending is shown as accepted, in every language.

API v1

API

Checkout.com runs on its Hosted Payments Page. Additive.

Changes

  • Checkout.com connections

    A Checkout.com connection now takes the customer to Checkout.com's Hosted Payments Page, where card details and 3-D Secure stay. Payments are authorized or captured at once, captured in full or in part, voided, and refunded in part. Each connection uses its account's own API address, so it asks for the Client ID and the processing channel beside the secret key. A refund Checkout.com accepts stays pending until its event or its record says it went through: in its sandbox, accepted refunds were later declined. Nothing changes in the API.

API v1 · SDK 0.9.3

API

API keys can be restricted to scopes. Additive.

Changes

  • Restricted keys

    Under Developers → API keys, a new key is full access or restricted to scopes: payments:read, payments:write, refunds:write, insights:read, suggestions:write, merchants:read and merchants:write. Each operation names the scope it needs under security in the OpenAPI document. A restricted key calling outside its scopes is answered 403 insufficient_scope, and the message names the scope. Every existing key is full access and behaves as before; a rolled key keeps its scopes. SDK 0.9.3 adds insufficient_scope to PayshenErrorCode; earlier versions report it as a PayshenAuthError with that code.

API v1

API

Five European acquirers and gateways. Additive.

Changes

  • Worldline, Worldpay, Nexi, Computop and Trust Payments

    Each connects through the provider's own hosted payment page, which also runs 3-D Secure, so card details never reach Payshen. A payment is captured at once, or authorized and later captured in full or in part, or released; refunds can be partial. The provider settles to the merchant's own account. Nexi connections use Nexi Checkout (formerly Nets Easy). Trust Payments accepts API calls only from addresses its Webservices user allows, so Payshen's outgoing IP addresses go on that list before testing. Nothing changes in the API: route to them like any other connection.

API v1

API

A Smart routing rule chooses only among the providers it lists. Routing behaviour change.

Changes

  • The providers on a Smart routing rule are a limit

    A rule in auto mode that lists providers now ranks and tries only those, and only the ones that can take the payment. Before, it ranked every provider that could take the payment and used the first two listed only as a starting point. A rule that lists none still chooses among every provider that can take the payment. When none of the listed providers can take it, the payment is refused with 409 routing_failed, as for a rule in any other mode, instead of going to a provider the rule leaves out. On the share Smart routing does not decide, payments now try the whole list in its order (routing.path rule); control now means a rule that lists no providers. Decisions are recorded under engine version 3.1.0. No field changed; no SDK update is needed.

API v1

API

A settled payment can be refunded. Additive.

Changes

  • Refunds from SETTLED

    POST /v1/payments/{id}/refunds now accepts a payment the provider has already settled, as well as captured and partially refunded ones. It was answered 409 not_refundable before. The refund goes to the provider like any other, under the same Idempotency-Key rules, and the payment moves to PARTIALLY_REFUNDED or REFUNDED once the provider confirms it. Nothing else changes. SDK 0.9.2 says so in payments.refund; earlier versions work too.

API v1

API

Six crypto payment gateways. Additive.

Changes

  • CoinsPaid, NOWPayments, CoinGate, Triple-A, BitPay and Coinbase

    Each connects through the gateway's own hosted page: the payment is priced in its own currency, the customer pays in crypto, and the gateway settles to the merchant's own account. A payment is captured only when the gateway says it is paid in full; an underpaid one stays open for review and is never captured for less, and an expired one is cancelled. Coinbase Commerce connections now use Coinbase Business checkouts, its successor, which also take partial refunds. Nothing changes in the API: route to them like any other connection.

API v1

API

Five more providers for Latin America and emerging markets, and the payer's national document. Additive.

Changes

  • customer.document

    The payer's national identity document number on a hosted payment: Brazil's CPF or CNPJ, Mexico's CURP, Argentina's DNI, Colombia's CC. dLocal requires it on every payment, and EBANX on most Brazilian ones. Like the rest of customer it reaches only the provider the customer is sent to and is never stored on the payment. SDK 0.9.1 types it.

  • dLocal, EBANX, Rapyd, Unlimint and Ecommpay

    Each connects through its own hosted page, with signed callbacks, status lookups and partial refunds; Unlimint also captures and releases card pre-authorizations. Nothing changes in the API: route to them like any other connection.

API v1

API

The routing engine's suggestions, by API and webhook. Additive.

Changes

  • GET /api/v1/suggestions and /api/v1/suggestions/{id}

    The suggestions the dashboard shows as cards, for the key's environment. A suggestion carries a monthly figure in value only when the lower end of its 95% interval is above zero; otherwise value is null and value_reason says why (claim: worth_testing is a gain 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 against what it predicted.

  • Accept, reject and snooze

    POST /api/v1/suggestions/{id}/accept applies it as the dashboard does, never repointing live traffic in one call: a 10% routing test, or draft rules a person publishes. It always requires an Idempotency-Key. /reject takes an optional reason; /snooze hides it for 7 days. A decided one answers 409 suggestion_not_open. API keys carry no narrower scope: any key of the workspace can decide suggestions in its environment.

  • suggestion.created and suggestion.updated webhooks

    suggestion.created when the engine has a new one; suggestion.updated when its state changes or what it realized is measured or changes verdict (changed says which). An endpoint that subscribes to every event receives them. SDK 0.9.1 adds suggestions.list/get/accept/reject/snooze.

API v1

API

A fee is never a placeholder: expected_fee says where every estimate came from. Additive.

Changes

  • expected_fee.source: learned and list_price

    Besides rules and headline (the merchant's contract), source can be learned, a rate learned from what the provider actually charged in its settlements and confirmed by the merchant, or list_price, the provider's published list price when no contract is on file. When nothing prices a connection, expected_fee is null: Payshen no longer assumes 2.50% for a connection nobody priced. SDK 0.9.1 types the new values.

  • fee_amount after settlement

    Once the provider's settlement report names a payment, fee_amount is the fee it took, in the payment's currency.

API v1

API

Partner provisioning takes the merchant's country and checks where Payshen onboards. Additive.

Changes

  • country on POST /v1/merchants

    White-label partners can send country, the ISO code of the country the merchant's company is registered in; it is stored on the merchant's default legal entity. A few countries regulate payment software even when it never touches funds, so Payshen keeps a country gate. A country Payshen does not onboard answers 422 country_blocked. A country under review is provisioned as usual; the merchant can set everything up in test mode, and going live (a live key, a live connection) and sending provider applications wait until Payshen clears the workspace. The SDK's CreateMerchantInput has it.

API v1

API

What to change next, by API: the Insights findings, ranked by the money they are about.

Changes

  • GET /api/v1/insights

    The 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 and routing advice. Each carries impact (an estimate over payments already seen, in your reporting currency, or null when the evidence is too thin), confidence, sample, why and the dashboard_path where it is fixed. Rate limited to 20 calls a minute; the SDK method is insights.list(). impact.basis is lower_bound (the lower end of a 95% interval, read as "at least") or estimate, and impact_reason says why a figure is null: an approval gap between providers is compared on first attempts only and carries no money until a routing test measures it.

API v1

API

fee_amount is only the fee your provider recorded; Payshen's estimate is the new expected_fee.

Changes

  • fee_amount means one thing

    fee_amount is the fee the provider recorded, reported on the charge or learned at settlement, and 0 until then. A capture used to store Payshen's headline-rate estimate there, so the field held a guess on some payments and money on others. If you booked fee_amount at capture, book it at settlement instead.

  • expected_fee

    A new object on the payment, the create answer, the capture, void and refund answers, and the payment.approved, payment.authorized and payment.captured webhooks: Payshen's estimate from the connection's pricing rules in force when the payment was made, else its headline rate. It carries amount, a low/high range when the card's type or region leaves one open, source (rules or headline) and the rule that applied. Null when no connection holds the payment yet, or the rules cannot price it in its currency. SDK 0.9.0 types it as ExpectedFee.

API v1

API

An optional customer on hosted payments, for providers that require one. Additive.

Changes

  • customer on POST /v1/payments

    Some high-risk and iGaming providers refuse a payment unless they know who is paying. Send customer (name, email, phone, address, date of birth, nationality, IP; every field optional) on a hosted payment and Payshen passes it to that provider only. It is kept sealed until the customer is handed over, then deleted; it is never stored on the payment, returned or put in a webhook. A payment routed to a provider that needs a field you did not send is refused before anything reaches the provider, naming what is missing. The SDK's CreatePaymentInput has it from 0.8.0.

SDK 0.7.0

SDK

Typed webhook event names. Additive; nothing you use changes.

Changes

  • WEBHOOK_EVENT_TYPES and WebhookEventType

    The event types Payshen sends, exported as a list and a type, and WebhookEvent.type typed with it. It stays open-ended, because v1 may add event types: keep a default branch in your switch. A test keeps the list equal to the API's documented webhooks.

API v1

API

Webhooks are part of the published contract, and refunds a provider only accepted now settle from its confirmation.

Changes

  • A webhooks section in the OpenAPI document

    One entry per event your endpoint can receive, with the headers (X-Payshen-Signature, X-Payshen-Event), the envelope (WebhookEvent), the fields each event's data carries, and how your answer is read. The reference renders the same list.

  • Retries, stated exactly

    The docs said a 4xx is never retried. Only 400, 410 and 422 end delivery of an event; a 401, 403, 404, 408 or 429 is retried like a 5xx, because it is usually a deploy or a secret rotation in progress. The schedule is 1 min, 5 min, 15 min, 30 min, 1 h, 2 h, 4 h, 8 h and 12 h apart: ten attempts over about 28 hours.

  • payment.created and payment.cancelled carry payment_id and environment

    Like every other payment event. Additive: id was and is the payment id.

  • Refunds that wait on the provider settle from its confirmation

    A refund the provider only accepted answers 409 refund_pending, and the same Idempotency-Key answers the final result once the provider confirms. For providers that report a refund only in a callback (Adyen), that confirmation now settles the refund and sends payment.refunded or payment.refund_failed; before, such a refund stayed pending. A refund still pending after a day is flagged on the payment.

  • Dashboard: keys and endpoints have their own menus

    API keys moved from Settings to Developers. Roll makes a new secret for a key while the old one keeps working until you revoke it, and an endpoint's Roll secret replaces its signing secret. Send test payment under Providers sends a test payment through one connection, whatever your routing rules say.

API v1

API

The OpenAPI document now describes every field the payment endpoints send. No API behaviour changed.

Changes

  • Payment fields that were on the wire but not in the spec

    Retrieve and list have always sent country, method, card_brand, initiator, capture_method, decline_code_raw, fee_amount, connection_id, psp_ref, merchant_entity_id, created_at and updated_at, and retrieve sends attempts. The spec now lists them, with a new PaymentAttempt schema, and says which fields only the create answer carries. A client generated from the old document simply did not see them.

  • Capture, void and refund answer their own shape

    The spec said these three answer a whole Payment. They have always answered the payment's money figures after the operation: id, payment_id, reference, status, amount, currency, captured_amount, refunded_amount, refund_pending_amount, fee_amount, provider_ref and environment. That is now PaymentOperationEnvelope, the same shape @payshen/sdk 0.6.0 types as PaymentOperationResult.

  • decline_category casing, said out loud

    The create answer and the payment.declined webhook carry it lowercase (authentication_failed); retrieve, list and each attempt carry it UPPERCASE. That has been true since v1 shipped and is unchanged; the spec now says so and lists the categories. Compare it case-insensitively.

  • Removed from the spec: reason on a refund

    The refund body documented a reason field that the endpoint never read or stored. It is gone from the spec; a request that still sends it is accepted as before and the field is ignored, as it always was. routing.mode is now documented as nullable, which it is on an answer rebuilt from an older payment.

API v1

API

A capture or void the provider only accepted answers PENDING, not done.

Changes

  • PENDING until the provider confirms

    Some providers (Adyen, Solidgate) accept a capture or a void and report the result later. Such an answer is now 200 with status: "PENDING": nothing is captured or cancelled yet, and payment.captured or payment.voided follows when the provider confirms it. Before, it was reported as done at once. Providers that answer synchronously, such as Stripe, are unchanged.

@payshen/sdk 0.6.0

SDK

On npm. The first release since 0.3.0, so it carries 0.4.0 and 0.5.0 too. If you page through payments or events, upgrade: lists stopped after their first page.

Changes

  • Lists stopped after their first page

    payments.list and events.list read the cursor from a field the API never sent, so nextCursor was always null and a loop on it looked finished after one page. Both read the API's next_cursor now, and pages carry hasMore. payments.iter(), which threw a TypeError on its first page, works.

  • Error codes kept, and no retry where the API says not to

    Every 422 used to become validation_error; the server's own code (idempotency_key_reuse, invalid_amount, no_eligible_method, ...) is now kept. Answers such as payment_result_unknown, refund_result_unknown and provider_unavailable are returned at once instead of retried: retrying payment_result_unknown showed you a 409 in place of the answer saying the provider may have the payment. PayshenErrorCode lists every code.

  • UNKNOWN, simulated, and the embedded checkout

    "UNKNOWN" is in PaymentStatus (0.4.0): the provider may have moved the money, so treat it like PENDING, never as a decline. simulated is typed: never fulfil an order against true. ui: "embedded" on payments.create (0.5.0) returns the collector and client_secret that @payshen/checkout needs, and the create result types its outcome fields and routing.

  • capture, void and refund return PaymentOperationResult

    They were typed as returning a whole Payment, whose other fields were undefined at runtime. Code that read them now fails to compile instead; read the payment with payments.get when you need them. The full notes are in the package's CHANGELOG.md.

@payshen/checkout 0.3.0

SDK

The browser package for embedded payments, on npm for the first time.

Changes

  • Card fields in your page, the card never on your servers

    Create the payment with ui: "embedded" on your server, then createCheckout, mount and confirm in the browser. It mounts the routed provider's own fields (Stripe Elements, Checkout.com Frames, or the sandbox simulator), so the card goes from the browser to the provider. A 3-D Secure challenge runs in the page and Payshen then asks the provider for the outcome.

  • complete(), typed statuses and errors

    complete() asks Payshen what became of the payment without charging, for a challenge left unfinished or on the page the customer returns to. CheckoutResult.status is the API's PaymentStatus, and PayshenCheckoutError is exported with every code it can carry. Your server still learns the outcome from the payment.* webhook or payments.get.

API v1

API

A payment no routing rule matches follows a fixed, predictable order.

Changes

  • Default order instead of the adaptive engine

    With no matching rule, a payment now goes to your oldest eligible connection first and fails over down your connections in the order you added them, exactly as a priority rule would. Its routing decision reads mode: "priority", path: "rule" and rule: null. Before, such a payment was scored by the adaptive engine (mode: "auto"). The adaptive engine is unchanged and now runs only for a rule you set to it. No field was added or removed.

API v1

API

A hosted payment can offer the customer a choice of how to pay.

Changes

  • allowed_methods on payments.create

    Send allowed_methods with your return_url and the hosted page offers the methods one of your connections can actually take for that exact payment, in your order of preference; the customer picks one and payment.method is what they chose. Sending method alone behaves exactly as it always has. The field is additive: nothing in the v1 contract changed.

  • A checkout is never opened with nothing to offer

    If no active connection can take any of the methods you allow, the create is refused 422 no_eligible_method and the message names each method and why. That refusal is deliberate: the alternative is sending a customer to a page where every choice ends in a decline that was decided before they clicked.

API v1

API

A provider can now settle a payment by calling back, verified in that provider's own signature scheme.

Changes

  • One callback endpoint per provider connection

    Each provider connection has its own callback URL, /api/v1/provider-webhooks/<connection id>, shown on the connection's page with a field for the signing secret that provider issues for it. Payshen verifies every delivery with that secret before it can move anything, stores the raw event before interpreting it, and answers a repeat of the same event 200 without doing the work twice. Nothing changes for payments that settle synchronously.

  • What a callback is allowed to do

    It can only move the payment held by that connection, and only along the lifecycle: a late event that would undo a captured payment, an amount that disagrees with ours, or a payment of another merchant is kept, shown to Payshen operations and applied to nothing. A status the provider uses and we do not map is stored as it arrived rather than guessed at. When a callback does settle a payment you get the normal payment.* webhook, the same one a synchronous settlement sends.

  • Stripe deliveries are verified as Stripe documents them

    The Stripe-Signature header's timestamp and every v1 signature (there is more than one while an endpoint secret is rotating), the payload as <timestamp>.<raw body>, HMAC-SHA256 under the endpoint secret, inside Stripe's own five minute window.

API v1

API

A capture, void or refund of a real provider's payment is never answered by the sandbox simulator.

Security

  • provider_unavailable: the operation is refused, not simulated

    A payment whose provider connection could not be reached from the deployment - its stored credentials were removed, or live provider access was off there - fell back to the sandbox simulator for capture, void and refund. The simulator keeps no record of a real provider's payment, and confirmed the operation anyway: a refund of a live charge was recorded as refunded, under a simulated refund reference, while no money moved. Such an operation is now refused with 503 provider_unavailable before the payment is claimed, so nothing changes, nothing is written and the Idempotency-Key is released for a later attempt. A payment the sandbox itself issued still completes in the sandbox, as it always did.

API v1

API

A Stripe card payment that needs customer authentication is declined with words that say so, and is neither retried nor cancelled.

Changes

  • stripe_requires_action: the customer must authenticate

    When Stripe answers a server-to-server create with a PaymentIntent in requires_action (the card issuer wants 3D Secure or similar), the payment is DECLINED with decline_category: authentication_failed and the raw code stripe_requires_action, as before. Stripe did not report it authorized or succeeded, so it was not charged. What changed is what it says: the documentation and the transaction page now state that the customer must complete authentication, which a server-to-server payment cannot show, and that Payshen does not retry it on another provider and does not cancel the provider's payment. Ask the customer to pay again through a checkout where they can authenticate.

API v1

API

Capture and void answer their own error code when the provider did not answer, so a refusal and an unknown result no longer look alike.

Changes

  • capture_result_unknown and void_result_unknown

    A capture or void whose provider did not answer (a 5xx, a timeout, a dropped connection, an answer that cannot be read) now answers 502 capture_result_unknown or 502 void_result_unknown. The provider may have executed it: the payment is UNKNOWN, do not retry, and the same Idempotency-Key replays this answer while Payshen settles the payment from the provider's record. 502 psp_error on these endpoints now means only that the provider refused: nothing was executed, the payment is AUTHORIZED again, and the key is released. Before, both cases answered psp_error and only the message told them apart. If you switch on psp_error for capture or void, add the two new codes and treat them as "do not retry".

API v1

API

Idempotency-Key is required on POST /v1/payments with a production (ps_live_) key, and a retry under the same key does not reach a provider again once one may have received the payment.

Security

  • A retried payment create could be charged twice

    When POST /v1/payments failed after the provider had been called (the payment could not be recorded, or its answer could not be stored), the Idempotency-Key was released and a retry under the same key routed the payment again. A request whose process died mid-call was routed again after 15 minutes, and in automatic routing the second run could pick a different connection, where the provider's own idempotency key protects nothing. The customer could be charged twice. Now every provider attempt is recorded against the key before the call, and once one is, a retry is answered from what was recorded: the stored response, else the payment's current state, else UNKNOWN with the payment put in front of Payshen operations. It is not sent to a provider again while the key is kept: 24 hours after its answer, and for as long as the attempt has no answer or its payment is UNKNOWN. After that the same key is a new request. A request that died and was never retried is turned into an UNKNOWN payment by a daily job.

Changes

  • Idempotency-Key required with production keys

    A POST /v1/payments with an API key whose environment is production (a ps_live_ key) and no Idempotency-Key is refused 400 idempotency_key_required. Sandbox keys (ps_test_) are unchanged. The SDK already sends a key on every create, so SDK users need do nothing; raw HTTP callers must send one unique key per payment and reuse it on retry.

  • 500 payment_result_unknown

    A provider may have received the payment and its result could not be recorded yet. The error carries request_id. Retry only with the same key, never with a new one: the retry never charges again, answers 409 for up to 15 minutes, then the payment as recorded (often UNKNOWN).

API v1

API

refunded_amount now counts only refunds the provider confirmed, a new refund_pending_amount holds the rest, and a refund Idempotency-Key names the refund itself.

Security

  • A refund that failed at the provider with a 5xx was reported as refused

    On a Stripe connection, a refund that Stripe answered with a 5xx was read as a refusal. The API answered 502, the amount became refundable again, and the Idempotency-Key was released. Stripe may execute a refund and still fail to answer, so the customer could have been refunded while the payment said they were not, and a later refund of a different amount could refund them beyond what the payment recorded. A 5xx, a dropped connection or an answer that cannot be read is now an unknown result: the amount stays reserved and is resolved from the provider's own refund list, and nothing retries it.

  • refunded_amount could count a refund that had not happened

    refunded_amount, and the REFUNDED and PARTIALLY_REFUNDED statuses, included refunds still at the provider and refunds whose answer never came. A payment could read REFUNDED for money that had not moved. Both now count only refunds the provider confirmed.

Changes

  • refund_pending_amount on every payment

    Minor units reserved for refunds sent to the provider and not confirmed yet, including one whose answer never came. It is on GET /v1/payments/{id}, the list, every capture, void and refund response, and the dashboard CSV export. It is not refunded, and it cannot be refunded again until it is resolved. A payment can read CAPTURED with refunded_amount: 0 and refund_pending_amount: 5000; that is a refund waiting on the provider, not a failed one.

  • The Idempotency-Key on a refund names the refund

    A request to POST /v1/payments/{id}/refunds under a key already used on that payment is answered from the refund the key names and never reaches the provider again: the stored success, the stored refusal, or 409 refund_pending while the provider has not confirmed it. This holds after the stored response expires. The same key with a different amount answers 422 idempotency_key_reuse. A 502 or 500 refund answer is kept under its key rather than released, so retrying it returns the same answer. Without a key, every request is a new refund, as before. Capture and void keep the old rule (a 5xx releases the key) for now.

  • refund_result_unknown: an unknown refund result has its own code

    A refund whose provider did not answer (a 5xx, a dropped connection, an answer that cannot be read) now answers 502 refund_result_unknown. Do not retry it under any key: the provider may have refunded, the amount stays in refund_pending_amount, and Payshen settles it from the provider's record. 502 psp_error on this endpoint now means only that the provider refused: nothing was refunded, the same key answers the same refusal, and a retry under a new Idempotency-Key is a new refund and is safe. Before, both cases answered psp_error and the documentation told you not to retry either.

  • A refund the provider accepted and has not finished is not reported as refunded

    On a Stripe connection, a refund Stripe answered with status pending or requires_action was reported as refunded. It now answers 409 refund_pending, its amount is in refund_pending_amount, and it counts as refunded only once Stripe says succeeded. A refund Stripe answers failed or canceled is a refusal. An answer with no refund id or no known status is refund_result_unknown. Payshen asks Stripe about accepted refunds once a day and settles them; you receive payment.refunded when one is confirmed.

  • How long a refund Idempotency-Key replays

    For 24 hours a repeated request replays the stored response exactly. After that, the same key on the same payment is answered from the refund's current record, so a refund that was pending when you first asked answers its final result.

  • payment.refund_failed: a pending refund that did not go through

    A new webhook event. When a refund held in refund_pending_amount (the provider accepted it and had not finished, or never answered) is settled as not refunded, from the provider's answer or by Payshen operations from the provider's record, you receive payment.refund_failed with refund_id, refund_amount, refunded_total, refund_pending_amount and failure_reason. The amount is no longer reserved and was not refunded. Endpoints subscribed to all events receive it; an endpoint with an event list receives it once you add it. A refund the provider refuses at once is still answered on the request itself and sends no event.

  • 409 refund_pending when only a pending refund is left

    When the whole remaining balance is held by a refund the provider has not confirmed, a new refund answers 409 refund_pending instead of 422 invalid_amount with an impossible range. Do not retry it; it clears when the pending refund is resolved.

API v1

API

A new status, UNKNOWN: the provider may have executed and we could not tell. Treat it like PENDING, never like DECLINED.

Security

  • A capture or void with no answer is no longer reported as failed

    Before 14 September 2026 such a capture or void put the payment back to AUTHORIZED, which could invite a second capture. It now ends UNKNOWN and is not retried. If you capture yourself, never repeat a capture on a payment that is UNKNOWN.

  • Fixed on 15 September 2026: a payment with no answer is never sent to a second provider

    Since 15 September 2026 a payment whose provider did not answer ends UNKNOWN and is never tried with another provider; it is settled from the provider's own record. Nothing to change in your integration. If you have a DECLINED payment from before that date with decline reason provider_error, check it with support before charging the customer again.

Changes

  • UNKNOWN is now a member of the PaymentStatus enum

    It means exactly one thing: the provider may have executed the operation and we could not tell. It is in flight, not an outcome, and never a success by default. It appears after a payment, a capture or a void whose provider call did not answer, and you can read it on GET and in webhooks, wherever a status can appear. It is settled from the provider's own record, and the payment then moves to a confirmed status. A refund whose provider call did not answer keeps its amount reserved and is written on the timeline as payment.refund_unknown.

  • What to do

    Treat UNKNOWN exactly as you treat PENDING: show "processing", wait for the webhook or poll the payment, and never render it as declined or repeat the capture, void or refund yourself. Resolving it is our job, against the provider's record. If your code switches on the status with a default branch that means "anything else is a failure", that branch is now wrong; add the case. The OpenAPI document carries the value and this description.

  • Why this is additive and not a v1 break

    v1 promised that no value would be removed or change meaning, and that new ones may be added, so parse defensively. This is the first time that promise is exercised on an enum. Nothing that existed moved; one value was added, on 14 September 2026, and a client with an exhaustive switch has to add a case. That is the behaviour the freeze was written to allow.

  • @payshen/sdk 0.4.0 carries the new value in the type

    The PaymentStatus union gains "UNKNOWN" and nothing else changes. It is a minor version rather than a patch because the union widened: a switch with exhaustive checking fails to compile until the new case is handled. That is the point - a status meaning "we do not know whether you were charged" must not fall through to whatever handled the old default, and the compiler is the one reader who never skips a changelog. Prepared alongside this entry; see the SDK guide for the client.

@payshen/sdk 0.3.0

SDK

If you handle webhooks: deduplicate on event.id. claimWebhookEvent does it for you.

Security

  • A verified webhook can arrive more than once

    Affects every SDK version before 0.3.0, whose documentation said verification also rejected repeats; it never did. Webhooks are delivered at least once, so handle each event.id once: from 0.3.0, claimWebhookEvent does that for you.

Changes

  • claimWebhookEvent and parseWebhookEvent

    claimWebhookEvent verifies, parses and claims the event id against a store you provide, returning the event the first time and null when it has already been handled. The store has one atomic method, claim - one INSERT ... ON CONFLICT DO NOTHING - because a separate read and write lets two concurrent deliveries both through. parseWebhookEvent verifies and returns the typed event when you want to claim inside your own transaction. See webhooks in the SDK guide.

  • WebhookEvent.created_at was never sent

    The field on the wire is created. Code that trusted the type read undefined with the compiler agreeing. The type is corrected, which is why this is a minor version: any code that read created_at now fails to compile, instead of failing quietly at runtime.

API v1

API

POST /v1/payments now answers the status enum it always documented. Check your branch on it.

Changes

  • status on the create response was the one field that broke the contract

    Creating a payment answered "approved" - lowercase, and not a member of the PaymentStatus enum our own OpenAPI document declares for that field. Every other endpoint already answered from that enum: reading the same payment a moment later returned "CAPTURED", and so did capture, void and refund. So if (res.data.status === "CAPTURED") was false for a payment that had captured, and the redirect half of this same endpoint answered "REQUIRES_ACTION" - the two halves did not agree with each other.

  • What changed, and what to do

    status now carries the documented value, identical to payment_status beside it: CAPTURED, AUTHORIZED, DECLINED, and the rest of the enum. If you compared against "approved" or "authorized" on the create response, compare against the uppercase value - or read payment_status, which has always been correct and is unchanged. Nothing else on the response moved.

  • Why this is a correction and not a v1 break

    v1 is frozen against changes of meaning, and the meaning here did not change - the published contract already said this field is a PaymentStatus. What changed is that the endpoint now keeps it. A generated client built from the spec was already broken against the old behaviour; one written by hand against observed behaviour needs the one-line change above.

@payshen/sdk 0.2.0

SDK

Upgrade from 0.1.0 if you take payments.

Security

  • Retries carry an idempotency key

    Affects 0.1.0, fixed in 0.2.0: a retried POST could create a second payment. Every POST now carries an idempotency key, yours or one generated by the client, reused across every attempt of that call.

  • A plain-http base URL is refused

    The API key travels in a header on every request. Over plain HTTP that header is readable by anything on the path, so a misconfigured base URL leaked live credentials silently. http://localhost stays allowed, because that is where mock servers run.

  • Replace a webhook example copied before 19 August 2026

    The webhook example on this site before 19 August 2026 did not check signatures correctly. If you copied it, replace it with the form on the SDK page, which a test now guards.

Changes

  • A reused AbortSignal no longer accumulates listeners

    One listener was added per retry attempt and never removed, which on a long-running process reached Node's max-listeners warning and held memory that should have been released.

@payshen/sdk 0.1.0

SDK

First published release. Superseded by 0.2.0; upgrade.

Changes

  • The client

    Payments (create, retrieve, list, capture, void, refund), the hosted flow, events, the partner endpoints, webhook verification, typed errors, bounded retries with jitter, timeouts and AbortSignal. Zero dependencies; ESM, CommonJS and types.

API v1

API

v1 is frozen.

Changes

  • The contract will not move underneath you

    Paths, request fields, response fields and error codes will not be removed or change meaning within v1. New optional fields and new endpoints may be added, so parse defensively and ignore what you do not recognise. Anything breaking ships as /api/v2 alongside this, never in place of it.

  • The /v1/transactions aliases were removed

    They duplicated /v1/payments with a different status filter, which is the kind of ambiguity that becomes permanent once a contract is frozen. Removed before the freeze rather than carried forever. Use /api/v1/payments.

  • The contract is published

    OpenAPI 3.1, unauthenticated, so it can be imported into Postman or used to generate a client without an account.