# Payment Links (https://dev.cosmospay.lat/docs/api/payment-links/)

> The payer’s view of one payment link, served without a key to the hosted checkout on payments.cosmospay.lat. The link id is the capability: anyone holding it can read the amount, the address to pay and the live status, and nothing about the merchant.

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

## Read a payment link as its payer sees it (no API key)

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

For a hosted checkout page: amount, asset, network, the address to pay (`payTo`), the link and its QR, and the live status, so the page can poll until the payment settles. Reachable without an API key or the gateway secret.

**The id is the capability.** Anyone who holds a payment link can read it here, which is what a payment link is for. Nothing about the owning merchant account, the payer’s own account, the envelope, the merchant’s `reference` or `callback`, or the status history is returned. `uri` is the link verbatim, so whatever the wallet needs from it is in it.

Answered with `Cache-Control: no-store`. Budgeted per client address (`x-cosmos-rate-limit`): poll at most every few seconds and honour `Retry-After`.

No authentication.

**Path parameters**

- `id` (string, required): The payment intent id (a cuid), as in the link `https://payments.cosmospay.lat/<id>`.

**Responses**

- `200` (`PublicPaymentLinkEntity`): OK
  - `id` (string, required): The payment intent id. It is also the capability: anyone holding it can read this.
  - `kind` (string, required): `PAY`: a link the wallet builds the payment from. `TX`: a Stellar envelope built for one known payer, carried inside `uri`. One of: `TX`, `PAY`.
  - `chain` (string, required): Chain the payment settles on. One of: `stellar`, `solana`, `monad`.
  - `network` (string, required): Network tier: `public` (mainnet) or `testnet` (Stellar testnet, Solana devnet, Monad testnet). One of: `public`, `testnet`.
  - `status` (string, required): The stored status. An unpaid intent past `expiresAt` reads `PENDING` until the observer expires it, and `EXPIRED` is not final: a payment verified on-chain later still moves it to `SUCCEEDED`. One of: `PENDING`, `SUBMITTED`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `EXPIRED`.
  - `payTo` (string, required): The address the payer sends to, always the one inside `uri`. Stellar and Solana: the merchant’s account. Monad with a relayer: the intent’s own deposit address, which forwards the payment to the merchant less `networkFee`.
  - `amount` (string, required): Decimal, in the asset’s units. Null: the payer chooses.
  - `asset` (string, required): Asset code, or `native` for the chain’s coin (XLM, SOL, MON).
  - `assetIssuer` (string, required): Stellar: the issuer. Solana: the SPL mint. Monad: the ERC-20 contract. Null for the native coin.
  - `memo` (string, required): The intent’s MEMO_ID. On Stellar the payment must carry it, and the link does.
  - `msg` (string, required): The message the merchant attached for the payer.
  - `uri` (string, required): The payment link, exactly as the wallet opens it: SEP-7 on Stellar, Solana Pay on Solana, EIP-681 on Monad. It is the same link the merchant handed out, so it carries what the wallet needs: a SEP-7 `callback` the merchant set and, for a `TX` intent, the unsigned envelope.
  - `qr` (string, required): QR code of `uri` (PNG data URL), derived on read.
  - `txHash` (string, required): The transaction reported or verified for this intent.
  - `networkFee` (string, 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): A fee the merchant charges on this payment, paid to its own wallet in the same transaction; show it to the payer. With `paidBy: payer` the payer sends `amount` plus it. Null when there is none.
  - `payerAccountRequired` (boolean, required): True when the payment cannot be one link (a Stellar link with a merchant fee: SEP-7 `pay` pays one account). Ask the payer for their account, then `POST /v1/payment-links/{id}/transaction` builds the envelope to sign; `uri` is the hosted checkout then, not a wallet link.
  - `expiresAt` (string (date-time), required): When an unpaid intent expires. A payment verified later still settles it.
  - `createdAt` (string (date-time), required): When the intent was created (ISO 8601).
  - `checkoutTheme` (string, required): The design the merchant chose for this checkout page; render it. `classic` when none was chosen. One of: `classic`, `split`, `ticket`, `card`.
- `400`: ValidationFailed (see Shared responses)
- `404` (`ApiErrorBodyEntity`): No payment intent has this id: it never existed or it was deleted. The two are the same answer, and the same one `GET /v1/payment-intents/{id}` gives its owner.
  - `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.
- `429`: RateLimited (see Shared responses)
- `500`: InternalError (see Shared responses)

## Build the envelope a payer signs for a Stellar link with a merchant fee (no API key)

`POST /v1/payment-links/{id}/transaction`

A SEP-7 `pay` link pays one account, so a Stellar payment link with a merchant fee (`payerAccountRequired` on the link) asks for its payer: given their account, this builds the unsigned envelope with both payments (to the merchant, and the fee to its wallet) and the link’s MEMO_ID, and its `web+stellar:tx` link and QR. The link settles on any transaction that pays both with the memo, so nothing is stored and a lapsed envelope is simply built again.

Only for an open link (`PENDING`). Budgeted per client address.

No authentication.

**Path parameters**

- `id` (string, required): The payment intent id, as in the link.

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

- `account` (string, required): The Stellar account that will pay and sign: the envelope is built on its sequence number, so it must exist on the link’s network.

**Responses**

- `200` (`PayerTransactionEntity`): OK
  - `xdr` (string, required): The unsigned transaction envelope (base64 XDR): two payments, to the merchant and to the fee wallet, with the link’s MEMO_ID.
  - `uri` (string, required): The SEP-7 `web+stellar:tx` link a wallet opens to sign it.
  - `qr` (string, required): The QR of `uri`, as a PNG data URL.
- `400` (`ApiErrorBodyEntity`): `validation_failed`: the link pays its destination directly (open its `uri`), or `account` is not a Stellar account. `invalid_state_transition`: the link is no longer open. `trustline_missing`: the merchant no longer trusts the asset.
- `404` (`ApiErrorBodyEntity`): No payment intent has this id.
- `429`: RateLimited (see Shared responses)
- `500`: InternalError (see Shared responses)
- `503`: UpstreamUnavailable (see Shared responses)

## Report what the payer did with a wallet from the hosted checkout (no API key)

`POST /v1/payment-links/{id}/events`

The hosted checkout tells what the payer did with a wallet while the link is pending: `walletOpened` (the wallet link, the web wallet, the extension or Freighter was opened), `walletClosed` (the wallet came back without paying) and `sent` (the page saw the wallet send the payment). The owner reads them as `checkoutWalletAt`, `checkoutWalletClosedAt` and `checkoutSentAt` on the intent, to follow a payment that is not on-chain yet.

**Informational only:** the status changes on what the chain says, never on these. An event for a link that is no longer pending is ignored. Budgeted per client address.

No authentication.

**Path parameters**

- `id` (string, required): The payment intent id, as in the link.

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

- `event` (string, required): `walletOpened`: the payer opened a wallet from the page (the wallet link, the web wallet, the extension, Freighter). `walletClosed`: the wallet came back without paying (it declined, or the page was shown again first). `sent`: the page saw the wallet send the payment. One of: `walletOpened`, `walletClosed`, `sent`.

**Responses**

- `204`: Noted (or ignored: the link is not pending).
- `400` (`ApiErrorBodyEntity`): `event` is not one of the three.
- `404` (`ApiErrorBodyEntity`): No payment intent has this id.
- `429`: RateLimited (see Shared responses)
- `500`: InternalError (see Shared responses)

## Solana Pay transaction request, GET: the label and icon a wallet shows (no API key)

`GET /v1/payment-links/{id}/solana-pay`

A Solana payment link with a merchant fee is a Solana Pay transaction request (`solana:<this URL>`), because a transfer request names one recipient. A wallet reads this first, then POSTs the payer’s account.

No authentication.

**Path parameters**

- `id` (string, required): The payment intent id.

**Responses**

- `200` (`SolanaPayInfoEntity`): OK
  - `label` (string, required): Who requests the payment.
  - `icon` (string, required): An absolute https URL of an image the wallet shows.
- `400` (`ApiErrorBodyEntity`): `validation_failed`: the link is not a Solana Pay transaction request (only a Solana link with a merchant fee is), or `account` is not a Solana address. `invalid_state_transition`: the link is no longer open.
- `404` (`ApiErrorBodyEntity`): No payment intent has this id.
- `429`: RateLimited (see Shared responses)
- `500`: InternalError (see Shared responses)

## Solana Pay transaction request, POST: the transaction the payer signs (no API key)

`POST /v1/payment-links/{id}/solana-pay`

Given the payer’s `account`, the unsigned transaction (base64) with the link’s memo, the transfer to the merchant carrying the link’s reference, and the merchant fee’s transfer to its wallet (creating their token accounts first when missing, paid by the payer). The link settles from the ledger, found by its reference; nothing is stored. Only for an open link.

No authentication.

**Path parameters**

- `id` (string, required): The payment intent id.

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

- `account` (string, required): The payer’s account (base58), which pays the fee and signs.

**Responses**

- `200` (`SolanaPayTransactionEntity`): OK
  - `transaction` (string, required): The unsigned transaction (base64 wire bytes): the payment to the merchant, carrying the link’s reference, the merchant fee to its wallet, and the link’s memo. Fee payer: the account that asked.
  - `message` (string, optional): The link’s message, for the wallet to show.
- `400` (`ApiErrorBodyEntity`): `validation_failed`: the link is not a Solana Pay transaction request (only a Solana link with a merchant fee is), or `account` is not a Solana address. `invalid_state_transition`: the link is no longer open.
- `404` (`ApiErrorBodyEntity`): No payment intent has this id.
- `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)

## Shared responses

- **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.
- **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.
