# Payment Intents (https://dev.cosmospay.lat/docs/api/payment-intents/)

> Create, fetch, update, cancel and validate payment intents on Stellar (SEP-7), Solana (Solana Pay) and Monad (EIP-681).

Base URL: `https://api.cosmospay.lat/cosmos-api`

## Create a SEP-7 `tx` intent (source known → unsigned XDR + tx URI + QR)

`POST /v1/payment-intents/tx`

Stellar only: builds the unsigned envelope paying `destination` from `source` on the key's network (a `prod` key builds for mainnet, a `dev` key for testnet), plus a second payment to `merchantFee.wallet` when a merchant fee is set, and answers its XDR, `web+stellar:tx` URI and QR. `memo` (a numeric MEMO_ID, generated when omitted) is the idempotency key: a retry with the same memo and terms returns the stored intent, other terms are `409 idempotency_conflict`. The intent starts `PENDING`, emits `PAYMENT_INTENT_CREATED`, and the on-chain observer settles it when the payment lands.

Requires the `payments:write` scope. The shared public key may call it.

**Request body** (`application/json`, required): `CreateTxPaymentIntentDto`

- `source` (string, required): Payer's Stellar account: the transaction source.
- `destination` (string, required): Payee's Stellar account.
- `amount` (string, required): Amount as a decimal string (max 7 decimals).
- `assetCode` (string, optional): Asset code. Omit (or "XLM"/"native") for native lumens.
- `assetIssuer` (string, optional): Issuer account for a non-native asset.
- `memo` (string, optional): MEMO_ID (numeric uint64) for idempotency + on-chain identification. Auto-generated when omitted.
- `msg` (string, optional): SEP-7 `msg`: shown to the user in their wallet (≤ 300 chars).
- `callback` (string, optional): SEP-7 `callback` where the wallet POSTs the signed XDR, e.g. `url:https://...`.
- `checkoutTheme` (string, optional): Design of the hosted checkout page (`https://payments.cosmospay.lat/<id>`) for this intent. Omit for `classic`. Cosmetic: it is not part of the payment, so `PATCH /v1/payment-intents/{id}` can change it later, and a retry with the same memo returns the stored intent with its stored design. One of: `classic`, `split`, `ticket`, `card`.
- `merchantFee` (MerchantFeeDto, optional): Your own fee on this payment, paid in its asset to `wallet` (a Stellar account that can receive the asset, not `source` or `destination`) by a second payment in the same envelope. `paidBy: recipient` takes it from `amount` (the destination receives `amount` less the fee); `payer` adds it on top. The intent settles only on a transaction that pays both.

**Responses**

- `201` (`TxPaymentIntentEntity`): Created
  - `id` (string, required): Payment intent id.
  - `kind` (string, required): Always `TX` for this response. One of: `TX`, `PAY`.
  - `status` (string, required): Lifecycle status. `SUCCEEDED`, `FAILED` and `CANCELLED` are final; an `EXPIRED` intent still settles on a payment verified later. One of: `PENDING`, `SUBMITTED`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `EXPIRED`.
  - `chain` (string, required): Chain the payment settles on. TX intents are always Stellar. One of: `stellar`, `solana`, `monad`.
  - `network` (string, required): Network tier, on every chain: `public` (mainnet) or `testnet` (Stellar testnet, Solana devnet, Monad testnet), the one the API key is for.
  - `source` (string, required): Payer's Stellar account: the transaction source.
  - `destination` (string, required): Payee's Stellar account.
  - `amount` (string, required): Amount to pay, as a decimal string (at most 7 decimals).
  - `asset` (string, required): Asset code, or `native` for XLM.
  - `assetIssuer` (object, required): Issuer account of an issued asset; null for XLM.
  - `memo` (string, required): Mandatory MEMO_ID.
  - `chainReference` (object, required): How the payment is found off Stellar. Solana: the Solana Pay `reference` key. Monad with deposit addresses: the intent’s own deposit address, which the link pays. Null otherwise.
  - `networkFee` (object, required): Deducted before the merchant is paid, in the asset’s units: the relayer’s fee for forwarding a Monad deposit. Null when nothing is.
  - `merchantFee` (MerchantFeeEntity, required): Your merchant fee on this payment, paid to its wallet in the same transaction; null when the request named none.
  - `msg` (object, required): SEP-7 `msg` shown to the payer in their wallet.
  - `callback` (object, required): SEP-7 `callback` where the wallet POSTs the signed XDR (e.g. `url:https://...`).
  - `xdr` (string, required): Unsigned transaction envelope (base64 XDR).
  - `uri` (string, required): The SEP-7 `web+stellar:tx` link carrying the unsigned envelope.
  - `qr` (string, required): QR code of `uri` (PNG data URL), derived on read.
  - `txHash` (object, required): The transaction reported or verified for this intent, in its chain's own form (hex lowercase); null until there is one.
  - `reference` (object, required): Your own reference for the intent, set with `PATCH`; null until set.
  - `checkoutTheme` (string, required): Design of the hosted checkout page. `classic` when none was chosen. One of: `classic`, `split`, `ticket`, `card`.
  - `expiresAt` (object, required): When the intent expires if unpaid.
  - `checkoutSeenAt` (object, required): When the hosted checkout page last read the intent: null at creation.
  - `checkoutWalletAt` (object, required): When the payer last opened a wallet from the hosted checkout (its wallet link, the web wallet, the extension or Freighter), as the page reports it. Informational: the status changes only on the chain.
  - `checkoutWalletClosedAt` (object, required): When the wallet last came back without paying (it declined, or the page was shown again first), as the page reports it; cleared when a wallet opens again.
  - `checkoutSentAt` (object, required): When the hosted checkout saw the wallet send the payment, as the page reports it: a payment on its way, before the chain confirms it.
  - `createdAt` (string (date-time), required): When the intent was created (ISO 8601).
  - `updatedAt` (string (date-time), required): When the intent itself last changed (ISO 8601).
- `400` (`ApiErrorBodyEntity`): `trustline_missing`: the destination does not trust the issued asset being paid. The payer signs this transaction and the destination signs nothing, so only the destination account can add the trustline.
  - `statusCode` (number, required): Always the HTTP status of the response itself.
  - `code` (string, required): Stable machine-readable code. Branch on this, never on the message: messages are human-facing and may be reworded; codes are never renamed once published. Each response documents the subset it can return. One of: `insufficient_scope`, `no_authenticated_consumer`, `gateway_required`, `admin_console_only`, `not_found`, `validation_failed`, `payload_too_large`, `idempotency_conflict`, `operation_in_flight`, `invalid_state_transition`, `slippage_exceeded`, `insufficient_balance`, `no_path_found`, `trustline_missing`, `invalid_amount`, `invalid_memo`, `transaction_rejected`, `transaction_already_settled`, `recipient_not_registered`, `payment_transfer_mismatch`, `fee_transfer_mismatch`, `merchant_fee_transfer_mismatch`, `payer_mismatch`, `transaction_not_found`, `expired`, `network_unsupported`, `asset_unsupported`, `merchant_fee_unsupported`, `provider_error`, `provider_unavailable`, `quote_not_found`, `quote_expired`, `kyc_state_invalid`, `kyc_review_required`, `account_disabled`, `payload_expired`, `rate_limited`, `customer_wallet_taken`, `alias_taken`, `alias_name_invalid`, `alias_challenge_invalid`, `alias_signature_invalid`, `alias_recovery_invalid`, `alias_address_conflict`, `wallet_handshake_invalid`, `wallet_verifier_invalid`, `wallet_login_code_invalid`, `wallet_login_code_cooldown`, `wallet_session_invalid`, `wallet_signature_invalid`, `wallet_backup_invalid`, `wallet_backup_limit`, `wallet_account_mismatch`, `wallet_provider_unavailable`, `wallet_return_url_not_allowed`, `wallet_recovery_setup_refused`, `easy_recovery_not_found`, `easy_recovery_already_offered`, `easy_recovery_not_offered`, `easy_recovery_code_invalid`, `easy_recovery_burned`, `plugin_not_installed`, `plugin_consent_mismatch`, `plugin_rejected`, `plugin_quota_exceeded`, `plugin_failed`, `protected_payment_buyer_mismatch`, `protected_payment_signature_invalid`, `misconfigured`, `internal_error`.
  - `error` (string, required): The HTTP reason phrase for `statusCode`.
  - `message` (string | string[], required): Human-readable detail. An array when class-validator rejected the body.
  - `details` (object, optional): Machine-readable facts about this failure, present only on the codes whose example shows it: `easy_recovery_code_invalid` carries `{ "attemptsLeft": 4 }`. Absent everywhere else.
  - `path` (string, required): The path of the request that failed.
  - `timestamp` (string, required): When the envelope was produced, ISO 8601.
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `409` (`ApiErrorBodyEntity`): `idempotency_conflict`: an intent with this `memo` already exists for different payment details. Retry with the original request unchanged, or use a new memo (omit `memo` to have one generated). `operation_in_flight`: a concurrent create for the same memo changed it while this one was being created; retry the request.
- `429`: RateLimited (see Shared responses)
- `500`: InternalError (see Shared responses)
- `503`: UpstreamUnavailable (see Shared responses)

## Create a `pay` intent: a payment link and QR on Stellar, Solana or Monad

`POST /v1/payment-intents/pay`

Answers a link any wallet can pay and its QR: SEP-7 `web+stellar:pay` on Stellar (the default `chain`), Solana Pay on Solana, EIP-681 on Monad, on the key's network (a `prod` key gets mainnet, a `dev` key the test networks). Omit `amount` for an open amount, except on Monad when this deployment has no deposit addresses; with a `merchantFee`, `uri` is the hosted checkout on Stellar and a Solana Pay transaction request on Solana. `memo` (numeric, generated when omitted) is the idempotency key: the same memo and terms return the stored intent, other terms are `409 idempotency_conflict`. Emits `PAYMENT_INTENT_CREATED`; the on-chain observer settles the intent when it finds the payment (a native MON payment without a deposit address needs `validate`).

Requires the `payments:write` scope. The shared public key may call it.

**Request body** (`application/json`, required): `CreatePayPaymentIntentDto`

- `chain` (string, optional): Chain the payment settles on. Omit for Stellar. The network (mainnet or test network) is the one your API key is for, as on Stellar. One of: `stellar`, `solana`, `monad`.
- `destination` (string, required): Payee's account on the chain: Stellar G…, Solana base58, Monad 0x….
- `amount` (string, optional): Amount the destination should receive, in the asset’s own units (XLM, SOL, MON, or the token). Omit to let the user enter it (e.g. donations), but not on Monad, where the amount is part of how the payment is recognised. At most 7 decimals on Stellar; elsewhere, at most the asset’s own decimals.
- `assetCode` (string, optional): Asset code the destination receives. Omit for the chain’s coin (XLM, SOL, MON). For a Solana or Monad token, the ticker it is labelled with, alongside `assetIssuer`.
- `assetIssuer` (string, optional): Stellar: the issuer account. Solana: the SPL mint. Monad: the ERC-20 contract. Omit for the chain’s coin.
- `memo` (string, optional): MEMO_ID (numeric uint64): the idempotency key on every chain, and on Stellar the on-chain identification too (on Solana it is recorded with the SPL Memo program). Auto-generated when omitted.
- `msg` (string, optional): Shown to the user in their wallet (≤ 300 chars): SEP-7 `msg` on Stellar, the Solana Pay `message`. Not supported on Monad.
- `callback` (string, optional): SEP-7 `callback`, e.g. `url:https://...`. Stellar only.
- `checkoutTheme` (string, optional): Design of the hosted checkout page (`https://payments.cosmospay.lat/<id>`) for this intent. Omit for `classic`. Cosmetic: it is not part of the payment, so `PATCH /v1/payment-intents/{id}` can change it later, and a retry with the same memo returns the stored intent with its stored design. One of: `classic`, `split`, `ticket`, `card`.
- `merchantFee` (MerchantFeeDto, optional): Only with an `amount`: your own fee on this payment, paid in its asset to `wallet` (an account on the link's chain, not `destination`). `paidBy: recipient` takes the fee from `amount`; `payer` adds it on top. Stellar: a SEP-7 `pay` link pays one account, so `uri` is the hosted checkout, which asks for the payer and has `POST /v1/payment-links/{id}/transaction` build the envelope with both payments. Solana: `uri` is a Solana Pay transaction request (`/v1/payment-links/{id}/solana-pay`); a SOL fee goes to an existing account. Monad: needs deposit addresses (`merchant_fee_unsupported` without them); the deposit address pays the fee (PaymentSplitForwarder) and the link asks for the amount, plus the fee when the payer pays it.

**Responses**

- `201` (`PayPaymentIntentEntity`): Created
  - `id` (string, required): Payment intent id.
  - `kind` (string, required): Always `PAY` for this response. One of: `TX`, `PAY`.
  - `status` (string, required): Lifecycle status. `SUCCEEDED`, `FAILED` and `CANCELLED` are final; an `EXPIRED` intent still settles on a payment verified later. One of: `PENDING`, `SUBMITTED`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `EXPIRED`.
  - `chain` (string, required): Chain the payment settles on. TX intents are always Stellar. One of: `stellar`, `solana`, `monad`.
  - `network` (string, required): Network tier, on every chain: `public` (mainnet) or `testnet` (Stellar testnet, Solana devnet, Monad testnet), the one the API key is for.
  - `source` (object, required): Always null for PAY.
  - `destination` (string, required): Payee account on the intent's chain (Stellar G..., Solana base58, Monad 0x...).
  - `amount` (object, required): Amount to pay, as a decimal string in the asset's units. Null when the payer chooses it.
  - `asset` (string, required): Asset code, or `native` for the chain's coin (XLM, SOL, MON).
  - `assetIssuer` (object, required): Stellar: the issuer. Solana: the SPL mint. Monad: the ERC-20 contract. Null for the native coin.
  - `memo` (string, required): Mandatory MEMO_ID.
  - `chainReference` (object, required): How the payment is found off Stellar. Solana: the Solana Pay `reference` key. Monad with deposit addresses: the intent’s own deposit address, which the link pays. Null otherwise.
  - `networkFee` (object, required): Deducted before the merchant is paid, in the asset’s units: the relayer’s fee for forwarding a Monad deposit. Null when nothing is.
  - `merchantFee` (MerchantFeeEntity, required): Your merchant fee on this payment, paid to its wallet in the same transaction; null when the request named none.
  - `msg` (object, required): Message shown to the payer in their wallet: SEP-7 `msg` on Stellar, the Solana Pay `message` on Solana.
  - `callback` (object, required): SEP-7 `callback` (e.g. `url:https://...`). Stellar only.
  - `xdr` (object, required): Always null for PAY.
  - `uri` (string, required): The payment link: SEP-7 `web+stellar:pay` on Stellar, Solana Pay on Solana, EIP-681 on Monad. With a merchant fee it is the hosted checkout on Stellar and a Solana Pay transaction request on Solana.
  - `qr` (string, required): QR code of `uri` (PNG data URL), derived on read.
  - `txHash` (object, required): The transaction reported or verified for this intent, in its chain's own form (hex lowercase); null until there is one.
  - `reference` (object, required): Your own reference for the intent, set with `PATCH`; null until set.
  - `checkoutTheme` (string, required): Design of the hosted checkout page. `classic` when none was chosen. One of: `classic`, `split`, `ticket`, `card`.
  - `expiresAt` (object, required): When the intent expires if unpaid.
  - `checkoutSeenAt` (object, required): When the hosted checkout page last read the intent: null at creation.
  - `checkoutWalletAt` (object, required): When the payer last opened a wallet from the hosted checkout (its wallet link, the web wallet, the extension or Freighter), as the page reports it. Informational: the status changes only on the chain.
  - `checkoutWalletClosedAt` (object, required): When the wallet last came back without paying (it declined, or the page was shown again first), as the page reports it; cleared when a wallet opens again.
  - `checkoutSentAt` (object, required): When the hosted checkout saw the wallet send the payment, as the page reports it: a payment on its way, before the chain confirms it.
  - `createdAt` (string (date-time), required): When the intent was created (ISO 8601).
  - `updatedAt` (string (date-time), required): When the intent itself last changed (ISO 8601).
- `400` (`ApiErrorBodyEntity`): `validation_failed`: The body, query or headers failed validation `invalid_amount`: Not a Stellar amount, or not greater than zero `invalid_memo`: The memo is not the type this route requires
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `409` (`ApiErrorBodyEntity`): `idempotency_conflict`: an intent with this `memo` already exists for different payment details. Retry with the original request unchanged, or use a new memo (omit `memo` to have one generated). `operation_in_flight`: a concurrent create for the same memo changed it while this one was being created; retry the request.
- `429`: RateLimited (see Shared responses)
- `500`: InternalError (see Shared responses)
- `502`: UpstreamError (see Shared responses)
- `503`: UpstreamUnavailable (see Shared responses)
- `504`: UpstreamTimeout (see Shared responses)

## List the consumer's payment intents

`GET /v1/payment-intents`

Newest first, as `{ data, total, take, skip }` with `take` up to 100 (default 20). Only intents on the key's network (a `prod` key lists mainnet intents, a `dev` key test-network ones), narrowed by `status`, `chain` and `createdFrom` when given.

Requires the `payments:read` scope.

**Query parameters**

- `status` (string, optional): Only intents in this status. Omit for every status. One of: `PENDING`, `SUBMITTED`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `EXPIRED`.
- `chain` (string, optional): Only intents on this chain. Omit for every chain. One of: `stellar`, `solana`, `monad`.
- `createdFrom` (string (date-time), optional): Only intents created at or after this instant (ISO 8601). Omit for no lower bound. The dashboard uses it for its today / week / month / year views of the list.
- `take` (number, optional): Page size: how many items to return (1 to 100).
- `skip` (number, optional): How many items to skip before this page (offset).

**Responses**

- `200` (`PaymentIntentListEntity`): OK
  - `data` (PaymentIntentEntity[], required): The payment intents on this page, newest first.
  - `total` (number, required): Number of intents matching the filters, across every page.
  - `take` (number, required): Page size applied (1 to 100).
  - `skip` (number, required): Number of intents skipped before this page.
- `400`: ValidationFailed (see Shared responses)
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `500`: InternalError (see Shared responses)

## List the status-transition history for a payment intent

`GET /v1/payment-intents/{id}/transitions`

The intent's audit trail as an array, oldest first: every status change with `fromStatus`, `toStatus`, the `txHash` it carried, its `reason` and the `actor` that made it (`api`, `validate`, `observer` or `system`).

Requires the `payments:read` scope.

**Path parameters**

- `id` (string, required)

**Responses**

- `200` (`PaymentIntentTransitionEntity[]`): OK
  - `id` (string, required): Transition id.
  - `intentId` (string, required): Id of the payment intent that changed.
  - `fromStatus` (string, required): Status before the change. One of: `PENDING`, `SUBMITTED`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `EXPIRED`.
  - `toStatus` (string, required): Status after the change. One of: `PENDING`, `SUBMITTED`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `EXPIRED`.
  - `txHash` (object, optional): The transaction the change carried, if any.
  - `actor` (string, required): api | validate | observer | system
  - `reason` (object, optional): Why the status changed (e.g. `on-chain payment confirmed`), when recorded.
  - `createdAt` (string (date-time), required): When the change happened (ISO 8601).
- `400`: ValidationFailed (see Shared responses)
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `404`: NotFound (see Shared responses)
- `500`: InternalError (see Shared responses)

## Get a payment intent by id

`GET /v1/payment-intents/{id}`

One of the consumer’s intents, on any chain, with its QR rebuilt from `uri`. `status` moves as the on-chain observer settles or expires the intent, which the `PAYMENT_INTENT_*` webhooks also report, so polling this route is optional.

Requires the `payments:read` scope.

**Path parameters**

- `id` (string, required)

**Responses**

- `200` (`PaymentIntentEntity`): OK
  - `id` (string, required): Payment intent id.
  - `kind` (string, required): `TX`: an unsigned Stellar transaction built for a known payer. `PAY`: a payment link any wallet can pay. One of: `TX`, `PAY`.
  - `status` (string, required): Lifecycle status. `SUCCEEDED`, `FAILED` and `CANCELLED` are final; an `EXPIRED` intent still settles on a payment verified later. One of: `PENDING`, `SUBMITTED`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `EXPIRED`.
  - `chain` (string, required): Chain the payment settles on. TX intents are always Stellar. One of: `stellar`, `solana`, `monad`.
  - `network` (string, required): Network tier, on every chain: `public` (mainnet) or `testnet` (Stellar testnet, Solana devnet, Monad testnet), the one the API key is for.
  - `source` (object, required): Payer account (null for PAY intents).
  - `destination` (string, required): Payee account on the intent's chain (Stellar G..., Solana base58, Monad 0x...).
  - `amount` (object, required): Amount to pay, as a decimal string in the asset's units. Null for a `PAY` intent with an open amount.
  - `asset` (string, required): Asset code, or "native" for the chain’s coin (XLM, SOL, MON).
  - `assetIssuer` (object, required): Stellar: the issuer. Solana: the SPL mint. Monad: the ERC-20 contract.
  - `memo` (string, required): Mandatory MEMO_ID (auto-generated if not provided).
  - `chainReference` (object, required): How the payment is found off Stellar. Solana: the Solana Pay `reference` key. Monad with deposit addresses: the intent’s own deposit address, which the link pays. Null otherwise.
  - `networkFee` (object, required): Deducted before the merchant is paid, in the asset’s units: the relayer’s fee for forwarding a Monad deposit. Null when nothing is.
  - `merchantFee` (MerchantFeeEntity, required): Your merchant fee on this payment, paid to its wallet in the same transaction; null when the request named none.
  - `msg` (object, required): SEP-7 msg.
  - `callback` (object, required): SEP-7 callback.
  - `xdr` (object, required): Unsigned transaction envelope (null for PAY intents).
  - `uri` (string, required): The payment link: SEP-7 on Stellar (`tx` for TX intents, `pay` for PAY), a Solana Pay transfer request on Solana, EIP-681 on Monad.
  - `qr` (string, required): QR code of `uri` (PNG data URL), derived on read.
  - `txHash` (object, required): The transaction reported or verified for this intent, in its chain's own form (hex lowercase); null until there is one.
  - `reference` (object, required): Your own reference for the intent, set with `PATCH`; null until set.
  - `checkoutTheme` (string, required): Design of the hosted checkout page. `classic` when none was chosen. One of: `classic`, `split`, `ticket`, `card`.
  - `expiresAt` (object, required): When an unpaid intent expires. A payment verified later still settles it.
  - `checkoutSeenAt` (object, required): When the hosted checkout page (payments.cosmospay.lat/{id}) last read the intent while it was open: the page polls while a payer has it open, so a value from the last 30 seconds means someone is on the payment page now. Updated at most every 10 seconds; null until a page opened it.
  - `checkoutWalletAt` (object, required): When the payer last opened a wallet from the hosted checkout (its wallet link, the web wallet, the extension or Freighter), as the page reports it. Informational: the status changes only on the chain.
  - `checkoutWalletClosedAt` (object, required): When the wallet last came back without paying (it declined, or the page was shown again first), as the page reports it; cleared when a wallet opens again.
  - `checkoutSentAt` (object, required): When the hosted checkout saw the wallet send the payment, as the page reports it: a payment on its way, before the chain confirms it.
  - `createdAt` (string (date-time), required): When the intent was created (ISO 8601).
  - `updatedAt` (string (date-time), required): When the intent itself last changed (ISO 8601).
- `400`: ValidationFailed (see Shared responses)
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `404`: NotFound (see Shared responses)
- `500`: InternalError (see Shared responses)

## Update a payment intent (status / txHash / reference / checkoutTheme)

`PATCH /v1/payment-intents/{id}`

Send only what changes; `reference` and `checkoutTheme` are accepted in any status. `status: SUCCEEDED` needs a `txHash` and goes through the same on-chain check as `validate` (`400 transaction_rejected` when the chain does not confirm it); other status changes follow the transition graph (`SUCCEEDED`, `FAILED` and `CANCELLED` are final, `EXPIRED` only settles on a verified payment), else `400 invalid_state_transition`. A new `txHash` alone is accepted only while `PENDING` or `SUBMITTED`. Emits `PAYMENT_INTENT_CANCELLED`, `PAYMENT_INTENT_SUCCEEDED` or `PAYMENT_INTENT_FAILED` for those statuses and `PAYMENT_INTENT_UPDATED` otherwise.

Requires the `payments:write` scope.

**Path parameters**

- `id` (string, required)

**Request body** (`application/json`, required): `UpdatePaymentIntentDto`

- `status` (string, optional): The new status, following the transition graph. `SUCCEEDED` needs a `txHash` and is checked on-chain like `validate`. One of: `PENDING`, `SUBMITTED`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `EXPIRED`.
- `txHash` (string, optional): The transaction once it is submitted, in its chain’s own form: a Stellar hash (64 hex), a Solana signature (base58) or a Monad hash (0x + 64 hex). Hex is stored lowercase. A transaction already recorded on another of your payment intents is refused with 409 `idempotency_conflict`.
- `reference` (string, optional): Merchant reference.
- `checkoutTheme` (string, optional): Design of the hosted checkout page (`https://payments.cosmospay.lat/<id>`). Accepted in any status, final ones included, alone or with the other fields: it changes nothing about the payment. One of: `classic`, `split`, `ticket`, `card`.

**Responses**

- `200` (`PaymentIntentEntity`): OK
- `400` (`ApiErrorBodyEntity`): `validation_failed`: The body, query or headers failed validation `invalid_state_transition`: The resource cannot move to that state from the one it is in `transaction_rejected`: The submitted transaction does not do what the route needs
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `404`: NotFound (see Shared responses)
- `409` (`ApiErrorBodyEntity`): `transaction_already_settled`: the transaction already settled, or also pays an older, payment intent of any consumer, and one payment settles at most one intent, the oldest it pays; this intent is not paid by it. `idempotency_conflict`: the hash is already recorded on another of your intents. `operation_in_flight`: the status changed under this request; re-read the intent and retry.
- `500`: InternalError (see Shared responses)
- `502`: UpstreamError (see Shared responses)
- `503`: UpstreamUnavailable (see Shared responses)
- `504`: UpstreamTimeout (see Shared responses)

## Delete a payment intent

`DELETE /v1/payment-intents/{id}`

Deletes an intent in any status but `SUCCEEDED` and emits `PAYMENT_INTENT_DELETED`. A paid intent is a settled payment and cannot be deleted (`400 invalid_state_transition`); an intent whose status changes during the delete (it may have just been paid) is `409 operation_in_flight`.

Requires the `payments:write` scope.

**Path parameters**

- `id` (string, required)

**Responses**

- `200` (`DeletedEntity`): OK
  - `id` (string, required): Id of the deleted payment intent.
  - `deleted` (boolean, required): Always true.
- `400` (`ApiErrorBodyEntity`): `validation_failed`: The body, query or headers failed validation `invalid_state_transition`: The resource cannot move to that state from the one it is in
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `404`: NotFound (see Shared responses)
- `409` (`ApiErrorBodyEntity`): `operation_in_flight`: the status changed under this delete. The intent may have just been paid. Re-read it before retrying.
- `500`: InternalError (see Shared responses)

## Validate a transaction against the intent and settle it

`POST /v1/payment-intents/{id}/validate`

Looks `txHash` up on the intent's chain and checks it pays this intent (success, destination, asset, amount, memo or reference, age); a match settles it `SUCCEEDED` and emits `PAYMENT_INTENT_SUCCEEDED`. This intent's own payment that failed on-chain marks it `FAILED` (`PAYMENT_INTENT_FAILED`); any other mismatch answers `valid: false` with a `reason` and leaves the status unchanged. An `EXPIRED` intent still settles on a verified payment, a `SUCCEEDED` one answers as it is without asking the chain, and one transaction settles at most one intent of any consumer (the oldest it pays).

Requires the `payments:write` scope.

**Path parameters**

- `id` (string, required)

**Request body** (`application/json`, required): `ValidatePaymentIntentDto`

- `txHash` (string, required): The transaction to validate against this intent, in its chain’s own form: a Stellar hash (64 hex), a Solana signature (base58) or a Monad hash (0x + 64 hex). It must be the one the intent’s chain uses.

**Responses**

- `200` (`ValidationOutcomeEntity`): OK
  - `valid` (boolean, required): True when the intent is `SUCCEEDED`: this transaction paid it, or it was already paid.
  - `status` (string, required): The intent status after the check. One of: `PENDING`, `SUBMITTED`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `EXPIRED`.
  - `reason` (string, optional): Why validation failed, when `valid` is false.
  - `paymentIntent` (PaymentIntentEntity, optional): The intent after the check; omitted when the check changed nothing.
- `400` (`ApiErrorBodyEntity`): `validation_failed`: The body, query or headers failed validation `transaction_rejected`: The submitted transaction does not do what the route needs `invalid_state_transition`: The resource cannot move to that state from the one it is in
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `404`: NotFound (see Shared responses)
- `409` (`ApiErrorBodyEntity`): `transaction_already_settled`: the transaction already settled, or also pays an older, payment intent of any consumer, and one payment settles at most one intent, the oldest it pays; this intent is not paid by it. `idempotency_conflict`: the hash is already recorded on another of your intents. `operation_in_flight`: the status changed under this request; re-read the intent and retry.
- `500`: InternalError (see Shared responses)
- `502`: UpstreamError (see Shared responses)
- `503`: UpstreamUnavailable (see Shared responses)
- `504`: UpstreamTimeout (see Shared responses)

## Shared responses

- **Unauthorized** (`ApiErrorBodyEntity`): `no_authenticated_consumer`: The gateway forwarded no API key identity
- **Forbidden** (`ApiErrorBodyEntity`): `insufficient_scope`: the API key does not hold the scope this route requires. The shared public API key also gets this on every route that is not marked `x-cosmos-public-key`, with a message saying so; more scopes will not help, the key is the wrong one. `gateway_required`: the request did not arrive through APISIX.
- **RateLimited** (`ApiErrorBodyEntity`): The budget in `x-cosmos-rate-limit` is spent for this caller. Honour `Retry-After`. The `ratelimit-*` triple rides along on successful responses too, so a client can pace itself instead of being refused. If the counter store itself is unreachable the guard fails closed and answers `503 provider_unavailable`.
- **InternalError** (`ApiErrorBodyEntity`): Unexpected server error. The detail is logged server-side and never returned: there is nothing in the body to branch on beyond the code.
- **UpstreamUnavailable** (`ApiErrorBodyEntity`): `provider_unavailable`: the provider in `x-cosmos-upstream` is down or unreachable; the message names it. Retry. `misconfigured`: this deployment is missing configuration the route needs (a provider instance, a fee wallet, the plan header the gateway must forward). Retrying will not help; it is an operator problem.
- **UpstreamError** (`ApiErrorBodyEntity`): The provider named in `x-cosmos-upstream` refused the request (`provider_error`) or could not be reached at all (`provider_unavailable`). Nothing was charged; retry is safe with the same `Idempotency-Key`.
- **UpstreamTimeout** (`ApiErrorBodyEntity`): The provider in `x-cosmos-upstream` did not answer in time. Whether it acted on the request is unknown. Retry with the same `Idempotency-Key`, never with a fresh one.
- **ValidationFailed** (`ApiErrorBodyEntity`): The body, query or path failed validation. `message` is an array of the individual failures. Routes that reject a *valid* request for a reason of their own (an amount, a memo, a state) document the code for it on their own 400.
- **NotFound** (`ApiErrorBodyEntity`): No such resource, or it exists and belongs to another consumer. The two are deliberately the same answer: "exists but not yours" is an ownership oracle.
