Changelog
Every entry states whether action is required. Entries marked “Required” will eventually cause a problem if left unaddressed; those marked “Not required” are purely additive and will not break your code without changes.
Our commitment on enum values: new statuses will be announced here in advance. Your default branch should therefore treat them as unknown and raise an alert, rather than pretending to recognize them. See each endpoint's status documentation.
This changelog has been maintained since 2026-08-12. Earlier changes were not recorded individually.
card_product_id is now optional in POST /v1/kyc/applications
No action requiredRequests already supplying card_product_id require no changes and behave as before. If you currently submit separately for every card BIN, you can simplify your implementation.
This endpoint submits one L1 application for the member, rather than a separate review for every card product. The first submission can now contain only profile. Once GET /v1/kyc becomes approved, call POST /v1/cards/applications with a different product_id to apply for other card products. Submitting for another product while the first application is still pending returns state_invalid. See KYC and Card issuance.
The new PATCH /v1/kyc endpoint lets a merchant server submit the profile fields to change directly, without a hosted page. All information can be updated field by field except the document number, nationality, document issuing country, and country of residence. Omitted information and images retain their original values. Updates trigger a new review, and the card product KYC status also becomes pending. Requires kyc:write, member context, request signing, and an idempotency key. See Update KYC directly.
card_id
Action requiredAfter receiving card.application.approved, retrieve the application details once.
GET /v1/cards/applications/{id} now always returns card_id: it is null before a card is created and crd_<uuid> afterward. Use it to call GET /v1/cards/{id}. Do not infer which card belongs to this application from the card list, BIN, or timestamps.
The webhook remains a lightweight notification without card details; retrieval is an idempotent read-only operation.
livemode now matches the data environment
No action requiredYou do not need to change requests or reconfigure endpoints.
Business data created with sandbox keys has always belonged to the internal shadow entity <merchant>_sbx, while webhook endpoints belong to the main merchant's operating configuration. The old routing incorrectly looked for endpoints under the shadow entity and marked sandbox events as livemode: true when the caller did not explicitly supply an environment. The result was only a no_subscriber record, with no request sent to any address.
Sandbox events now inherit the main merchant's subscriptions. The payload's merchant_id is always the main merchant's short code, with livemode: false. Matching sandbox endpoints on the main merchant are preferred; if none exist, delivery falls back to existing live endpoints for compatibility. Production events still go only to live endpoints.
We also corrected redelivery of no_subscriber events: all matching endpoints now receive the event, rather than only the first. Redelivery keeps the original event_id, and each endpoint retries independently.
network now consistently uses machine-readable codes; GET /v1/assets includes networks; quote_id is no longer silently ignored
Action requiredAction required (item ① is a breaking change).
① network in GET /v1/merchant/deposit-addresses now uses a machine-readable code. Previously it returned a display name (BNB Smart Chain (BEP20)), while POST for the same resource accepted a machine-readable code (BSC). Sending the value received from GET unchanged to POST always resulted in asset_not_allowed. This contradictory contract is now consistent: network always contains a machine-readable code, and the new network_name supplies the display name. If you stored this field and used it for display, read network_name instead. If you hardcoded a mapping table as a workaround, you can remove it.
② This page previously documented the BSC machine-readable code as BEP20, which does not exist. The correct value is BSC; BEP20 appears only in the display name. Requests following the old documentation and sending BEP20 are rejected.
③ GET /v1/assets now supplies networks[] for each asset. This is the only authoritative list of machine-readable codes. It also supplies values that were previously unavailable: per-network deposit_enabled / withdraw_enabled (unavailable networks are included with reasons), min_deposit / confirmations_required / memo_required, and withdrawal-side min_withdraw / max_withdraw / withdraw_fee_* / withdraw_cooling_hours / withdraw_eta_minutes. Do not hardcode the network list. We add an asset by adding an administrative record, without a release; a hardcoded list cannot follow that change.
④ Member exchange price locking now works: quote_id is supported. Call POST /v1/exchange/quotes with lock: true to obtain a quote_id, then supply it to POST /v1/exchange/orders to execute at that quote. Expired quotes are always rejected; the order never falls back to a new price. quote_id is the order ID itself. If execution times out, use GET /v1/exchange/orders/{quote_id} to check whether it executed.
⚠ Request a lock explicitly (lock defaults to false). There are two reasons: this endpoint is called frequently just to display a number, so unconditional order creation would fill your order list with unexecuted quoted rows; and a locked quote gives you a free option lasting quote_ttl_sec seconds, so every requested lock must be an explicit, countable, auditable record.
⚠ The lock covers the price, not authorization. All checks still run at execution time: account restrictions, disabled directions, strong authentication, limits, and balances. Without a lock, quote_ttl_sec remains indicative only.
Background: this fixes an inconsistency on our side. An endpoint comment once told merchants wanting a price lock to supply quote_id when ordering, but quotes never returned an ID and orders never read it. Integrations following that advice believed they had locked a price, while execution actually used the new price at the time of the trade, leaving them to absorb the difference without any indication. There was briefly an explicit rejection (quote_lock_unsupported) when the field was supplied. Now that the capability is implemented, that code no longer occurs for member exchange; its only current application is merchant self-conversion (POST /v1/merchant/conversions), which does not yet support price locking.
data.id; previous_status removed
Action requiredAction required (both changes are small, but leaving them unaddressed will eventually cause problems):
① data.id in card.application.approved and card.status.updated. Previously these contained the internal member ID, while data.object was card_application / card. Retrieval inevitably returned 404. They now contain the actual object ID: the application ID (retrieve with GET /v1/cards/applications/cap_<id>) or card ID (retrieve with GET /v1/cards/crd_<id>). Remove any special case in your handler that treats these two card event IDs as member IDs. Both events now also include the actual status_version. Card status changes can arrive consecutively (freezing → frozen); merge forward by version.
② Event bodies no longer contain previous_status. It was always an empty string, with no producers. If you wrote if (data.previous_status === …), that branch never matched; use the current value in your own database instead. We will not populate an actual previous value: duplicate delivery of the same transition is normal, and on the second delivery the “previous state” already equals the new state. An incorrect value is more dangerous than no value.
The other 7 events whose status_version is always 0 are not pending fixes. Each reason is documented in the event catalog and Webhook guide. Continue processing those by arrival order plus retrieval to confirm the current state.
Previously, POST /v1/sandbox/upstream-behavior only stored a value; none of the four upstream clients read it (as recorded in the 2026-08-12 entry). Injection now actually replaces the response to the provider call that moves funds, after which we process it with exactly the same logic as production.
You can therefore exercise three paths that are normally impossible to trigger on demand in the sandbox: timeout / unknown (→ outcome uncertain; funds remain locked), reject (→ terminal failure; locked funds released), unknown_status (→ suspended with an alert; no default to “processing”).
No action is required, but we strongly recommend an additional integration test: inject timeout and exercise your uncertain-outcome branch. It is the branch most likely to be implemented without testing, and an error there can result in both a refund and a payment. See Sandbox.
⚠ Read-only calls to the same provider (quotes, decoding, and queries) are unaffected and still reach the real upstream. These two endpoints still return 404 in live mode.
Valid business rejections such as insufficient funds, expired quotes, disabled directions, or invalid address checksums previously fell through the public code catalog's default branch and returned 500 api_error. Each now has an explicit 4xx code.
Action required: remove any workaround that retries 500 responses for these cases and branch on code instead. The new public code amount_out_of_range means the amount is outside the product range. Its handling is the opposite of limit_exceeded: that code means an exhausted allowance and requires waiting; this one can be resolved by changing the amount.
We also removed 21 unused codes with no emission sites and added a guard: an unregistered internal code, or a registered code that is never emitted, now fails the commit-time check.
GET /v1/cards/{id} · GET /v1/cards/{id}/transactions · GET /v1/earn/products/{id} · POST /v1/cards/{id}/replacements.
Card transactions now also return direction. amount is an absolute value, so a refund and a purchase look identical if you inspect only the amount.
GET /v1/remittances/{id} could previously return pending_merchant_funds (mapped to pending by both the list and creation endpoints), and source_amount was a fixed-point integer string without a decimal point. The same order therefore differed by a factor of 10^ledger_scale between endpoints.
Action required: remove any conversion you added to reconcile detail and list amounts. Both now use decimal strings and include ledger_scale alongside them.
GET /v1/merchant/custody now includes ledger_scale
No action requiredThe two reconciliation amounts are fixed-point integer strings. Previously their scale was omitted, even though /v1/merchant/statements for the same product line always supplied it. Merchants could interpret one endpoint correctly but had to guess for the other. null means the asset has been removed from the catalog; reconciliation tables retain historical rows.
Previously the spec only contained a method, path, one-line description, and scope, without any field declarations. Each endpoint now has its own page with parameter tables, request fields, response examples, and calling examples in three languages. API behavior has not changed; only the documentation has.
The new /errors page explains each code and what you should do: retry, avoid retrying, or seek human intervention.
Action required: if your retry logic branches on HTTP status, change it to branch on code. Some counterintuitive cases: service_unavailable, limit_exceeded, asset_not_allowed, and state_invalid are all 400, not 5xx.
The new /events page includes event payload examples and delivery semantics.
Action required: the catalog identifies the actual form of data.id and whether status_version is usable for each event. 9 events always have status_version 0. If your merger skips versions less than or equal to the current value, only the first of those 9 events will take effect. Until we fix this, use arrival order plus retrieval to confirm the state for these events.
{ data, next_cursor, has_more }
No action requiredStatic list endpoints, such as GET /v1/transfers/recipient-types, also return this envelope, so your generic paginator does not need special cases for them.
⚠ One exception remains: GET /v1/balances still returns { balances: [...] }. This is noted on its endpoint page; the fix will be recorded here.
Limits are counted per merchant × bucket: deposit reports 60/minute, member creation 300/minute, other writes 120/minute, and reads 1200/minute. Exceeding a limit returns 429 + Retry-After.
Action required: treat 429 as a normal path and back off according to Retry-After. A 429 response does not mean you have been banned. See Rate limits.
Previously, a single 2xx counted as complete success: with three configured endpoints, if one succeeded and two returned 500, the latter two would never be retried, yet the record showed delivered. Now one row represents one (event, endpoint) pair with independent backoff. The new no_subscriber state distinguishes a broken subscription from having no events at all.
Delivery rows now carry the environment, and subscription endpoint queries filter by environment. Sandbox endpoints no longer receive production events.
⚠ At that time, upstream fault injection was a no-op: calls still reached the real upstream after injection. Fixed on 2026-08-13 (see the latest relevant entry on this page). See Sandbox.