> ## Documentation Index
> Fetch the complete documentation index at: https://agents-docs.lightspark.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> New features, improvements, and updates to the Grid API.

Follow along with the latest additions and improvements to the Grid API. For upcoming
changes and roadmap, [contact our team](https://www.lightspark.com/contact).

**November 2026**

## Issue a business's card to a named person

`POST /cards` takes an optional `cardholder` for a `BUSINESS` customer: the person the
card is issued to, with their name, email, phone number and US residential address. The
card spends from the business's account and carries that person's name and contact
details, so one business can hold cards for many people. Cards with the same
`cardholder.reference` go to the same person. Omit `cardholder` to issue the card to the
business itself, as before.

**October 2026**

## Verify a new sign-in email or phone number before it replaces the old one

Changing a Global Account customer's `email` or `phoneNumber` on
`PATCH /customers/{customerId}`, when the account signs in with it, now needs a verified
replacement. Register the new address with `POST /auth/credentials`, verify its code with
`POST /auth/credentials/{id}/verify`, then send the customer update and its signed retry.
The signed retry can return `200` with `{ "status": "PROCESSING" }` while the change lands,
and `409 OPERATION_IN_PROGRESS` while another operation on that contact is in progress.

A change without a verified replacement returns `400 AUTH_CREDENTIAL_VERIFICATION_REQUIRED`
instead of a signed-retry challenge. The `EMAIL_OTP_CREDENTIAL_SET_CHANGED`,
`SMS_OTP_CREDENTIAL_SET_CHANGED`, `EMAIL_OTP_CREDENTIAL_SYNC_FAILED`, and
`SMS_OTP_CREDENTIAL_SYNC_FAILED` codes are retired. See
[Changing the sign-in email or phone number](https://agents-docs.lightspark.com/global-accounts/authentication.md#changing-the-sign-in-email-or-phone-number).

## Sandbox suffix for a paused corridor

In sandbox, an external account whose account number ends in `014` simulates a corridor
paused for maintenance. The account lookup and the quote fail with `403
  CORRIDOR_UNDER_MAINTENANCE`. Other accounts in the same currency work as normal, so you
can test the paused and the open case side by side. See
[sandbox testing](https://agents-docs.lightspark.com/payouts-and-b2b/platform-tools/sandbox-testing.md#testing-cross-currency-quotes).

## Sandbox no longer accepts placeholder signatures

The sandbox placeholders `Grid-Wallet-Signature: sandbox-valid-signature` and the passkey
assertion signature `sandbox-valid-passkey-signature`, deprecated earlier this month, are
no longer accepted. Sandbox rejects each as a malformed stamp or assertion with `401
  UNAUTHORIZED`, as production always has. Send a real API-key stamp over `payloadToSign`
on every signed action and the real WebAuthn assertion on passkey verify. See
[Sign a `payloadToSign`](https://agents-docs.lightspark.com/global-accounts/client-keys#2-sign-a-payloadtosign) and
[sandbox testing](https://agents-docs.lightspark.com/global-accounts/platform-tools/sandbox-testing.md).

## ACH `railDetails.returnCode` and `returnCategory`

A returned ACH entry's `railDetails` now carries `returnCode`, the NACHA return reason
code the receiving bank sent it back under, such as `R01` (insufficient funds) or `R10`
(customer advises unauthorized). It is present with `returnedAt` on returned payouts and
on returned pulls from your customers' bank accounts. Group returns by `returnCode` to
track your return rate for each reason. NACHA may add codes, so accept any value of the
form `R` followed by two digits.

`railDetails.returnCategory` names the Nacha return-rate category the code counts
toward: `ADMINISTRATIVE` (`R02`, `R03`, `R04`) or `UNAUTHORIZED` (`R05`, `R07`, `R10`,
`R11`, `R29`, `R51`). Nacha sets a separate threshold for each. A code in neither, such as
`R01`, has no `returnCategory`.

## `EXTERNAL_ACCOUNT.ACCOUNT_INFO_UPDATED` webhook

Grid now tells you when it corrects an external account's details. When the
receiving bank returns an ACH notification of change (NOC), Grid applies the
corrected routing number, account number or bank account type and sends
`EXTERNAL_ACCOUNT.ACCOUNT_INFO_UPDATED`. The payload is the full external account
with the corrected `accountInfo`. The account keeps its `id`, and later payments use
the corrected details. Deliveries can arrive out of order, so fetch the account
before you replace any copy of its details you store.

## Two new `cardDeclinedReason` values

A declined card transaction's `cardDeclinedReason` now names two more causes that were
reported as `OTHER`:

* `INCORRECT_CARD_DETAILS`: the PIN, security code, expiry date, or billing address
  entered at the merchant did not match the card.
* `TEMPORARILY_UNAVAILABLE`: a temporary problem at Grid, the card processor, or the
  card network stopped the authorization. The cardholder can try again.

Production already declines an authorization the funding source cannot cover, but has
reported it as `OTHER`. It now reports `INSUFFICIENT_FUNDS`, and the reference no longer
calls that value sandbox-only. `OTHER` now covers only the remaining reasons, including a
security or fraud decline the card processor made. Transactions declined before this
change keep their reason.

> [!WARNING]
>
> If you validate `cardDeclinedReason` against a fixed list, add the two new values, and
> treat any value you don't recognize as `OTHER`.

## Sandbox values for each payout `failureReason`

You can now test each specific payout `failureReason` in sandbox. Pay out to an
external account whose number ends in one of these suffixes, or to the matching UMA
address:

| Suffix | UMA address | `failureReason` |
| - | - | - |
| `010` | `$fail.payout.compliance.usd@sandbox.uma.money` | `COMPLIANCE_REJECTED` |
| `011` | `$fail.payout.accountinvalid.usd@sandbox.uma.money` | `ACCOUNT_INVALID` |
| `012` | `$fail.payout.cannotreceive.usd@sandbox.uma.money` | `ACCOUNT_CANNOT_RECEIVE` |
| `013` | `$fail.payout.limitexceeded.usd@sandbox.uma.money` | `LIMIT_EXCEEDED` |

The existing suffix `005` completes the payout, then fails it with `PAYOUT_RETURNED`.

## Set the ACH statement description with `statementDescriptor`

`POST /quotes` takes a new optional `statementDescriptor`: up to 10 characters that
label the payment on the counterparty's bank statement, such as `PAYROLL`. It is used
only on ACH, where it is sent as the NACHA Company Entry Description, for both a
payout to an external US bank account and a pull from one:

```json theme={null}
{
  "source": { "sourceType": "ACCOUNT", "accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965" },
  "destination": { "destinationType": "ACCOUNT", "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123", "paymentRail": "ACH" },
  "lockedCurrencySide": "SENDING",
  "lockedCurrencyAmount": 10000,
  "statementDescriptor": "PAYROLL",
  "remittanceInformation": "INV-12345"
}
```

Grid uses its default when you omit it. Sending it on a quote that moves no ACH entry
returns `400 INVALID_INPUT`. Longer payment details still belong in
`remittanceInformation`, which ACH carries in the addenda record. ACH `railDetails` on
transactions and webhooks reports the value as `statementDescriptor`.

The `remittanceInformation` description now also covers rails beyond ACH, RTP, FedNow,
and wire. On rails such as SEPA, PIX, SPEI, and Faster Payments it is sent as the payment
reference on the recipient's bank statement, which is often much shorter than 80
characters, so put the most important information first.

## List balance changes for periodic statements

`GET /internal-accounts/{id}/balance-changes` returns every change to an internal account's
balance between `startDate` and `endDate`, in the order the money moved. The window is
half-open, so consecutive windows leave no gap and do not overlap. The response also
carries the window's `openingBalance` and `closingBalance`, and the opening balance plus
the sum of every `amount` across all pages equals the closing balance.

Each change has an `id`, `amount`, `effectiveAt`, and an optional `transactionId`
that you can fetch from `GET /transactions/{transactionId}`. Fees are already inside
`amount`; a transaction's total fee is `fees` on the transaction. A window whose card settlement has not
closed returns `409 NOT_YET_AVAILABLE`. Retry once it has settled. To build a statement
from these changes, see the guides for [cards](https://agents-docs.lightspark.com/cards/statements.md) and
[payouts](https://agents-docs.lightspark.com/payouts-and-b2b/payment-flow/statements.md).

## Business customers can omit `taxId` and `incorporatedOn`

`businessInfo.taxId` and `businessInfo.incorporatedOn` are no longer required in the
schema when you create a business customer. They are still required when the customer's
currency uses direct customer-owned accounts, and Grid returns `400 INVALID_INPUT` naming
the missing field. When you fund customers from an omnibus FBO account, you can omit them.
For details, see [configuring customers](https://agents-docs.lightspark.com/ramps/onboarding/configuring-customers.md).

## API tokens can expire

`ApiToken` has a new optional `expiresAt`. When it is present, requests made with that
token after that time return `401`, and you need to create a new token. Tokens without
`expiresAt` do not expire.

## A UMA address destination requires `currency`

`UmaAddressDestination.currency` on `POST /quotes` is now marked required in the spec.
The API has always rejected a UMA address destination without it, so no working
integration changes. Generated clients now enforce it at build time.

## Quote and transfer creation answer `200`

`POST /quotes`, `POST /transfer-in` and `POST /transfer-out` have always answered
`200 OK` for a newly created quote or transfer, and the spec now says so instead of
`201 Created`. The `202 Accepted` reply for a quote awaiting Strong Customer
Authentication is unchanged.

## `railDetails` on transaction sources and destinations

Account and external-funding sources, and account destinations, carry a
`railDetails` describing the rail transfer that moved funds for that side. It is a
oneOf discriminated by `paymentRail`, and each rail has its own schema, status values,
and bank account fields. It covers the rails below; a side that moved over any other
rail has no `railDetails`, and the deprecated fields remain its source of rail
details.

| `paymentRail` | Schema | `status` values | Rail-specific fields |
| - | - | - | - |
| `ONCHAIN` | `OnChainRailDetails` | — | `transactionHash`, `network` |
| `ACH`, `ACH_SAME_DAY` | `AchRailDetails` | `PENDING`, `SETTLED`, `RETURNED`, `RETURN_DISHONORED`, `RETURN_CONTESTED`, `REVERSED`, `FAILED` | `traceNumber`, `addenda`, `returnedAt`, `dishonoredAt`, `contestedAt`, `reversedAt` |
| `WIRE` | `WireRailDetails` | `PENDING`, `SETTLED`, `FAILED` | `imad`, `originatorToBeneficiaryInformation` |
| `FEDNOW` | `FedNowRailDetails` | `PENDING`, `SETTLED`, `FAILED` | `endToEndId`, `remittanceInformation` |
| `RTP` | `RtpRailDetails` | `PENDING`, `SETTLED`, `FAILED` | `endToEndId`, `remittanceInformation` |

`railDetails.status` reports the transfer on its rail; the transaction's top-level
`status` reports where the funds are now, and its webhook events are unchanged. A returned
ACH payout still moves to `FAILED` with `PAYOUT_RETURNED` and is refunded as before; its
destination's `railDetails` now also shows `RETURNED` with `returnedAt`. An ACH deposit
returned after `COMPLETED` moves to `FAILED` and sends `INCOMING_PAYMENT.FAILED`, with the
source's `railDetails` showing `RETURNED`. A transaction stays `COMPLETED` through a return
only when a later step that can't be undone already used the funds.

`FAILED` is no longer described as terminal. On an incoming payment, a dishonored return
restores the credit and moves the transaction back to `COMPLETED`. A returned payout stays
`FAILED`, since it has already been refunded. Either way the ACH `railDetails` records the
step: `RETURN_DISHONORED` with `dishonoredAt`, and `RETURN_CONTESTED` with `contestedAt` if
the dishonor is contested. Only `EXPIRED` is terminal; apply each status webhook as it arrives. `TransactionStatus.REFUNDED`
is deprecated: a refund is reported on the transaction's `refund` object.

The flat originator fields on the external funding source (`accountHolderName`, `accountIdentifier`,
`bankName`, `bankIdentifier`, `paymentRail`, `remittanceInformation`, `endToEndId`,
`traceNumber`) are deprecated, as is `onChainTransaction`. They remain populated alongside
`railDetails`, and on rails without a `railDetails` variant, such as SWIFT, they are the only
source of the originator's details.

**September 2026**

## Counterparty failures a retry can fix return `5xx`

When a lookup or quote fails because of the counterparty and a retry could succeed, Grid
now returns a `5xx` status. Some of these failures returned `400`, which reads as "your
request was wrong". Existing error codes keep their names. Only their HTTP status moves.

* `LOOKUP_REQUEST_FAILED` returns `504` instead of `400`. A network error or timeout
  stopped a receiver lookup from reaching the counterparty.
* `QUOTE_REQUEST_FAILED` returns `504` instead of `400`. A network error or timeout
  stopped `POST /quotes` from sending the quote request to the counterparty.
* `INVALID_PAYREQ_RESPONSE` returns `502` instead of `400`. The counterparty returned an
  invalid payment request.
* The new `COUNTERPARTY_SERVER_ERROR` returns `502`. The counterparty answered a lookup
  or quote request with a server error. This includes a quote to a receiver whose
  provider is temporarily unavailable, which returned `400 INVALID_INPUT`.

A lookup for a UMA address whose domain doesn't exist is a bad address, not a retryable
failure. It now returns `404 USER_NOT_FOUND` instead of `400 LOOKUP_REQUEST_FAILED`.

If you already retry on `5xx`, these are retried with no code-specific handling. If you
detect them by matching `400` or `424`, match on `code` instead. See
[Error handling](https://agents-docs.lightspark.com/payouts-and-b2b/payment-flow/error-handling.md).

## Sandbox test suffixes work on Solana addresses

A sandbox external account's last three characters pick the test outcome, and every
failure suffix starts with `00`. Base58 has no `0`, so no valid Solana address could end
in one. The transfer and quote destination suffixes `002`–`008` now have `11X` twins: an
address ending in `112` behaves like `002`, and `117` like `007`. The `00X` forms still
work. See
[Sandbox testing](https://agents-docs.lightspark.com/api-reference/sandbox-testing.md).

## `customerId` is required when creating a customer external account

`POST /customers/external-accounts` has always rejected a request that omits
`customerId`, and the spec now says so. Its request body is its own schema,
`CustomerExternalAccountCreateRequest`, in which `customerId` is required. Use
`POST /platform/external-accounts` to create an external account owned by the
platform itself.

## Same-day ACH is its own payment rail

`ACH_SAME_DAY` joins `ACH` in `PaymentRail`. Pin it on a quote's destination to request
same-business-day settlement for a USD payout.

* Grid never selects `ACH_SAME_DAY` for you. It is only used when you name it, so
  existing integrations that omit `paymentRail` are unaffected.
* The two rails are priced separately. A `RailFeeConfig` with `rail: ACH_SAME_DAY`
  prices same-day sends; your existing `ACH` config keeps pricing standard sends.
* `ACH_SAME_DAY` is capped at $1,000,000 per entry by the NACHA same-day limit, rising
  to $10,000,000 on 2027-09-17. A payout above the limit is rejected rather than slowed;
  send it over `ACH` instead.
* `ACH` will settle on the standard next-business-day schedule. Until a date we
  announce in advance, `ACH` continues to settle same-business-day on production
  platforms; it already settles next-business-day on sandbox platforms. To guarantee
  same-day settlement after that date, request `ACH_SAME_DAY`.

See [Assessing fees](https://agents-docs.lightspark.com/payouts-and-b2b/payment-flow/assessing-fees.md).

## Add cards to Apple Pay, Google Pay, and Samsung Pay from your app

New `POST /cards/{id}/tokenize` exchanges the values a wallet SDK produces
for the encrypted provisioning payload that completes an in-app "Add to
Wallet" tap. The payload is encrypted to the wallet provider's keys, so
card data never crosses your servers. `Card.cardCapabilities` gains
`supportsDigitalWalletTokenization`. Manual entry into a wallet continues
to work for every card with no integration. See
[Digital wallet tokenization](https://agents-docs.lightspark.com/cards/card-management/digital-wallet-tokenization.md).

## Card refunds, declines, and voids report distinct outcomes

Three card-statement corrections to `CardTransaction`:

* **Refunds are their own dated rows.** A merchant `RETURN` against a purchase now opens
  its own `CardTransaction` (`direction: CREDIT`, credited value in `settledAmount`)
  linked back to the purchase via the new `originalTransactionId` field, instead of
  folding into the purchase's `refundedAmount`. The purchase stays `SETTLED`; the refund
  fires its own `CARD_TRANSACTION.*` webhooks on the refund row. To reverse a sandbox
  return, pass the refund row's id to `POST /sandbox/cards/{id}/simulate/return_reversal`.
* **`DECLINED` status with `cardDeclinedReason`.** An authorization declined before any
  money moved now reports `DECLINED` (and fires `CARD_TRANSACTION.DECLINED`) instead of
  `EXCEPTION`, which is reserved for settlement anomalies. The new `cardDeclinedReason`
  field explains why Grid declined the authorization: `CARD_NOT_ACTIVE` (frozen or closed
  card), `SPEND_LIMIT_EXCEEDED`, `INSUFFICIENT_FUNDS` (sandbox only—production approves
  underfunded auths and resolves them as `EXCEPTION`), `NO_ELIGIBLE_FUNDING_SOURCE` (e.g.,
  Embedded Wallet with no delegated key), `BLOCKED` (merchant category or transaction
  blocked), `UNSUPPORTED_NETWORK`, or `OTHER`. Declines must be excluded from cardholder
  statements.
* **`VOIDED` status.** An approved authorization that is fully reversed or expires
  before any clearing posts now reports `VOIDED` (and fires
  `CARD_TRANSACTION.VOIDED`) instead of `SETTLED`.
* Card rows now carry the same full currency metadata (`decimals`, `name`, `symbol`) as
  payment rows, and `merchant` gains `city` and `state` when the network reports them.
* `GET /transactions` accepts a `cardId` filter (`Card:` LSID or UUID) for listing one
  card of a multi-card cardholder, and rejects `status` combined with `type=CARD` with a
  400—`status` applies to payment transactions only.

## Record consent for each agreement by type

`agreementConsents` supersedes `endUserTermsConsent`: a list with one entry per agreement,
so a customer's acceptance of each document is recorded and auditable separately.

* Each entry carries a `type` alongside the existing `acceptedAt`, `ipAddress`,
  `termsVersion`, and `acceptanceMethod` fields. Send at most one entry per type.
* `GET /customers/agreements` lists every supported agreement with its `type`, current
  `version`, and hosted `url`. Send an entry's `version` as `termsVersion` when recording
  that agreement's acceptance. `GET /customers/end-user-terms` is deprecated but unchanged
  — it still returns the single `{ version, url }` object, so existing callers keep working.
* Customer responses return `agreementConsents`, holding the most recent acceptance per
  accepted type, and an empty list until the first acceptance.
* On update, supplying consents records additional acceptances; acceptances already on
  file for other types are left untouched.

`endUserTermsConsent` is **deprecated, not removed** — it still works on both the request
and response, and is equivalent to a single `LIGHTSPARK_END_USER_TERMS` entry. Acceptance
you have already recorded is migrated for you and reported under that type; you don't need
to re-collect it. Sending both fields in one request is rejected.

Accepting one agreement never implies acceptance of another, so the remaining six types
must be collected from each customer.

See [Disclosures](https://agents-docs.lightspark.com/payouts-and-b2b/onboarding/disclosures.md).

## Card returns report as `SETTLED` with a `refundedAmount`

`REFUNDED` is removed from `CardTransactionStatus`, and `CARD_TRANSACTION.REFUNDED` from
the webhook types. A card transaction's status tracks authorization outcomes and
settlement—a return is reported through `direction` and `refundedAmount`, which together
already carry it.

* A settled purchase that the merchant later returns stays `SETTLED`, with the returned
  value in `refundedAmount`. A standalone merchant refund arrives as `direction: CREDIT`
  and `status: SETTLED`, with the credited value in `settledAmount`.
* Returns no longer get a webhook type of their own—the transaction re-fires
  `CARD_TRANSACTION.SETTLED` with the updated amounts.
* The sandbox return reversal simulator requires a parent with a posted return (non-zero
  `refundedAmount`) rather than a `REFUNDED` parent, which the previous docs described
  and the server never produced.

Nothing you have received changes: the status was always derived on read, so existing
transactions already report `SETTLED`. `TransactionStatus.REFUNDED` on cross-border
payments is a separate enum and is unaffected.

See [Reconciliation](https://agents-docs.lightspark.com/cards/transactions/reconciliation.md).

## Sandbox KYC and KYB follow the production flow

Unregulated sandbox platforms now resolve a customer's verification from a submitted
packet rather than at create, so your sandbox integration rehearses the same collect,
submit, and resolve loop you run in production.

> [!WARNING]
>
> Behavior change. On an unregulated sandbox, a name suffix alone no longer sets the
> result at create. New customers stay `UNVERIFIED` until you call `POST /verifications`.
> Customers created before this change keep their status, and regulated sandbox platforms
> are unaffected.

* Upload documents with `POST /documents`, register beneficial owners for a business, then
  submit with `POST /verifications`. The suffix on `fullName`, `registrationNumber`, or a
  beneficial owner's `lastName` still decides the outcome—it applies once the submission
  is in rather than at create.
* An incomplete submission returns `verificationStatus: RESOLVE_ERRORS` naming every
  missing field and document, so you can fix and resubmit. A business whose
  `registrationNumber` ends in `002` now reports those errors first rather than rejecting
  outright.
* Renaming a customer no longer overwrites a settled result—`APPROVED` and `REJECTED` stay
  put. Create a new customer when you want to exercise a different outcome.
* To keep the previous fast path, open **Configuration** in your sandbox dashboard and turn
  on **Skip verification paperwork**. A terminal suffix then resolves with no documents:
  individuals at create, businesses at their first submission. `001` and `003` still require
  a complete packet. Turn it off when you want to test the full flow again.

Walk through the fix-and-resubmit loop in [sandbox testing](https://agents-docs.lightspark.com/api-reference/sandbox-testing.md).

**August 2026**

## `bankAccountType` is required on USD external accounts

Creating an external account with `accountType: USD_ACCOUNT` now requires
`bankAccountType`. Send `CHECKING` or `SAVINGS` on `POST /customers/external-accounts`,
`POST /platform/external-accounts`, and `POST /agents/me/external-accounts`.

* Grid uses `bankAccountType` to set the ACH transaction code. Accounts created without
  it were sent as checking, and the receiving bank returned a notification of change to
  correct savings accounts.
* External accounts created before this change are unaffected.

See [External accounts](https://agents-docs.lightspark.com/ramps/accounts/external-accounts.md) for a full request body.

## `/transfer-in` and `/transfer-out` are deprecated

Same-currency transfers now go through the quote endpoint, so one integration covers
same-currency and cross-currency alike.

* Use `POST /quotes` with `immediatelyExecute: true` to create and execute a
  same-currency transfer in a single request.
* `amount` becomes `lockedCurrencyAmount` with `lockedCurrencySide: "SENDING"`; source
  and destination gain `sourceType: "ACCOUNT"` and `destinationType: "ACCOUNT"`.
  `remittanceInformation`, `purposeOfPayment`, and the destination `paymentRail` carry
  over unchanged.
* The response is a `Quote` rather than a `Transaction`—read `transactionId` from it to
  track the resulting transaction.
* `POST /transfer-in` and `POST /transfer-out` continue to work with unchanged request
  and response shapes.

See [Send a payment](https://agents-docs.lightspark.com/payouts-and-b2b/payment-flow/send-payment.md#send-a-payment)
for the updated flow.

## Assess your own fees on every transaction

Charge your customers a platform fee and keep the margin—Grid collects it for you and
reports it separately from network and FX costs.

* Set a standing fee with `feeConfigs` on `PATCH /config`—a variable rate in basis
  points, a fixed amount, or both, applied to cross-currency transactions.
* Override it on a single transaction with `platformFeeOverride` on `POST /quotes`, for
  promos, VIP pricing, or negotiated rates. No standing config required.
* Both the standing config and the per-transaction override require a USD source
  currency today.
* Reconcile with `platformFeesIncluded` on quotes and `platformFees` on outgoing
  transactions—your cut, broken out of the total.

See [Fees](https://agents-docs.lightspark.com/platform-overview/core-concepts/quote-system.md#fees) for exactly how the
variable fee is assessed on each locked side.

## Record end-user terms acceptance

Unregulated platforms must record that each customer accepted Grid's end-user terms
before their account opens.

* `GET /customers/end-user-terms` returns the current terms URL and version.
* Pass `endUserTermsConsent` on customer create or update with the timestamp, IP address,
  terms version, and acceptance method.
* Quotes fail with `END_USER_TERMS_NOT_ACCEPTED` until acceptance is on file.

See [Disclosures](https://agents-docs.lightspark.com/payouts-and-b2b/onboarding/disclosures.md).

## Run KYC from your own onboarding form

Collect verification data in your own UI and submit it programmatically instead of
handing customers to a hosted flow.

* Enhanced due diligence—source of funds and wealth, purpose of account, expected
  transaction count and volume, income and net worth ranges, PEP status—is now a set of
  optional fields on the individual customer itself. Send them on `POST /customers` or
  add them later with `PATCH /customers/{customerId}`; there is no separate EDD resource.
* `POST /verifications` returns `RESOLVE_ERRORS` with one entry per problem, each naming
  the exact field or accepted document types still needed, so you can fix and resubmit
  without guessing.
* Request validation errors return every invalid field at once in
  `Error400.details.errors[]`, each with a machine-readable constraint you can render as
  field-level UX—no more resubmitting to discover the next error.
* Individual customers and beneficial owners now share one identification vocabulary:
  `idType`, `identifier`, and `countryOfIssuance`.

Rehearse the fix-and-resubmit loop in [sandbox testing](https://agents-docs.lightspark.com/api-reference/sandbox-testing.md).

## Groundwork for EU support: SCA and Travel Rule

The API surface for the two controls EU regulation requires is now defined, so you can
design against it ahead of EU corridors opening.

* **Strong Customer Authentication**—`POST /sca/login/complete` returns
  `sessionExpiresAt`, so you can prompt a re-login before the 180-day session lapses
  rather than discovering it as a failed payment. Quote authorization documents
  `SCA_SESSION_REQUIRED` (409) and `ACCOUNT_LOCKED` (423), and `SCA_NOT_COMPLETED`
  explains a transaction that failed on an expired challenge.
* **Travel Rule ownership verification**—a challenge and verify flow for proving a
  customer owns a self-custody wallet. `POST …/external-accounts/{id}/challenge` starts
  verification by wallet signature or hosted liveness check, and `…/verify` completes a
  signature synchronously. Accounts sit at `PENDING_OWNERSHIP_VERIFICATION`—still usable
  below regulatory thresholds—and move to `ACTIVE` on success or `UNVERIFIED` on a failed
  attempt. A single webhook, `EXTERNAL_ACCOUNT.STATUS_UPDATED`, covers the whole
  lifecycle.

EU availability will be announced separately.

## Issue your own stablecoin

Register a stablecoin with `/stablecoins`, then mint and burn directly against it with
`/stablecoins/{stablecoinId}/mints` and `/stablecoins/{stablecoinId}/burns`.
`/stablecoins/{stablecoinId}/operations` tracks each issuance through settlement.

## More ways to move money

* **USDT on Ethereum and Plasma**, alongside Tron.
* **Bitcoin L1** deposit addresses as a payment instruction on quotes.
* USD accounts can describe a full wire beneficiary—bank name, checking or savings,
  intermediary bank and routing number, and bank-to-bank instructions.
* Businesses outside the US can register as a publicly listed company, trust, private
  foundation, or charity.

See [Currencies and rails](https://agents-docs.lightspark.com/platform-overview/core-concepts/currencies-and-rails.md).

## Track crypto settlement to and from external wallets

A transaction that settles on-chain to or from an external crypto wallet now carries the
settled transfer inline: `onChainTransaction` on the transaction's source or destination
reports the transfer's `transactionHash` and `network` once the crypto transfer settles.

> [!WARNING]
>
> Deprecation. `reconciliationInstructions.transactionHash` is deprecated for wallet
> transfers—read `onChainTransaction` instead, which also tells you the network. The field
> keeps reporting the inter-VASP settlement leg of a UMA payment, which has no
> `onChainTransaction` equivalent, and will not be removed before that leg has a
> replacement.

## Know why a payment failed

Failure reasons now name outcomes you act on rather than internal processing steps.

> [!WARNING]
>
> Breaking change. `EXECUTION_FAILED_POST_DEBIT` and `SETTLEMENT_FAILED` are removed and
> collapse into `QUOTE_EXECUTION_FAILED`, whose description now covers the whole path to
> settlement and states that a debited amount is refunded automatically. `TIMEOUT` and
> `MANUAL_REFUND` are removed because that outcome already surfaces on the refund object.

* New payout failure reasons: `PAYOUT_RETURNED`, `LIMIT_EXCEEDED`,
  `ACCOUNT_CANNOT_RECEIVE`, `ACCOUNT_INVALID`, and `COMPLIANCE_REJECTED`.
* `pendingReason` on any transaction tells you when it is held for compliance review or
  waiting on customer action.
* New limit codes: `TRANSACTION_SIZE_LIMIT_EXCEEDED` (400) and
  `DAILY_VOLUME_LIMIT_EXCEEDED` (429).

See [Transaction lifecycle](https://agents-docs.lightspark.com/platform-overview/core-concepts/transaction-lifecycle.md#failure-handling).

**July 2026**

## Card issuance in sandbox

You can now issue cards directly in the sandbox environment. Test the full card
lifecycle—[cardholder setup](https://agents-docs.lightspark.com/cards/onboarding/cardholder-setup.md),
[issuing cards](https://agents-docs.lightspark.com/cards/card-management/issuing-cards.md),
[funding sources](https://agents-docs.lightspark.com/cards/card-management/funding-sources.md), and
[freezing and closing](https://agents-docs.lightspark.com/cards/card-management/freezing-and-closing.md)—end to end
before going live, without touching production.

See [Cards sandbox testing](https://agents-docs.lightspark.com/cards/platform-tools/sandbox-testing.md) to get started.

## Cards: real-time webhooks and full event simulation

React to card activity as it happens, and rehearse every event before going live.

* `CARD_TRANSACTION.*` fires on every state transition—authorized, partially settled,
  settled, refunded, and exception—carrying the full card transaction.
* All ten sandbox simulate endpoints, covering balance inquiries, credit and financial
  authorizations, authorization advices, returns, and return reversals.
* Brand the tokenization verification codes your customers receive with
  `cardTokenization2faConfig` on platform config.

See [Card webhooks](https://agents-docs.lightspark.com/cards/platform-tools/webhooks.md).

## Refund visibility on incoming payments

`INCOMING_PAYMENT.REFUND_PENDING`, `INCOMING_PAYMENT.REFUND_COMPLETED`, and
`INCOMING_PAYMENT.REFUND_FAILED` now fire the same way outgoing refunds already did, so
a returned deposit no longer surfaces only as a failed transaction.

See [Refund object](https://agents-docs.lightspark.com/platform-overview/core-concepts/transaction-lifecycle.md#refund-object).

## Expanded country coverage

Grid now supports additional countries, including **China**, broadening the corridors
available for global payments. Review the currencies and rails available in each region
in [Currencies and rails](https://agents-docs.lightspark.com/platform-overview/core-concepts/currencies-and-rails.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.