# Private Payments (https://dev.cosmospay.lat/docs/api/zk-payments/)

> Payments on Stellar whose amount stays hidden on-chain. Create a private payment, list and read them; the merchant receives the amount minus the private-payment commission (see https://cosmospay.lat/pricing).

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

## The private payments setup of the API key's network

`GET /v1/zk-payments/config`

What a dashboard needs to set a merchant up on the confidential token (register, read the balance, withdraw). `enabled` is false on a network where private payments do not run yet (mainnet today). The same shape as the keyless `GET /v1/zk-checkout/config?network=`.

Requires the `payments:read` scope.

**Responses**

- `200` (`ZkPaymentsConfigEntity`): OK
  - `enabled` (boolean, required): Whether private payments run on this network. False until the operator deploys the token there (mainnet today); every other field but `network`, `networkPassphrase`, `auditorId` and `feeBps` is then null.
  - `network` (string, required): The network the API key's environment selects. One of: `public`, `testnet`.
  - `rpcUrl` (string, required): The Soroban RPC to build and submit transactions through.
  - `networkPassphrase` (string, required): The Stellar network passphrase of `network`.
  - `token` (ZkTokenEntity, required): Cosmos Pay's confidential token on this network; null when `enabled` is false.
  - `verifier` (string, required): The UltraHonk verifier registry the token checks proofs with.
  - `auditorRegistry` (string, required): The auditor registry holding Cosmos Pay's key.
  - `auditorId` (number, required): The auditor id every account on the token registers under.
  - `feeBps` (number, required): The commission: 18 bps = 0.18%.
  - `feeAccount` (string, required): Cosmos Pay's account the commission is transferred to.
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `500`: InternalError (see Shared responses)

## List the consumer's private payments

`GET /v1/zk-payments`

Newest first, optionally filtered by `status` as it reads now (a `PENDING` payment past `expiresAt` reads, and filters, as `EXPIRED`). Paginated with `take` (default 20, max 100) and `skip`; the answer is `{ data, total, take, skip }`. Only payments on the key's network are listed (a `prod` key sees mainnet, a `dev` key testnet).

Requires the `payments:read` scope.

**Query parameters**

- `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).
- `status` (string, optional): Only payments in this status, as it reads now: `EXPIRED` includes a PENDING payment past `expiresAt`, and `PENDING` excludes one. One of: `PENDING`, `SUCCEEDED`, `EXPIRED`.

**Responses**

- `200` (`ZkPaymentListEntity`): OK
  - `data` (ZkPaymentEntity[], required): The private payments on this page, newest first.
  - `total` (number, required): How many private payments match the query, across all pages.
  - `take` (number, required): The page size this page was read with.
  - `skip` (number, required): How many matching payments were skipped before this page.
- `400`: ValidationFailed (see Shared responses)
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `500`: InternalError (see Shared responses)

## Create a private payment (0.18% commission)

`POST /v1/zk-payments`

A private payment hides its amount on-chain: it is paid with confidential transfers on Cosmos Pay's confidential token, proven with zero-knowledge proofs (hence `zk` in the paths). Prices the payment (`amount` = `netAmount` + `feeAmount`, the fee rounded up and at least one stroop) and checks on-chain that the recipient holds a confidential account on the token under auditor 0. The payer then makes two confidential transfers (net to the merchant, fee to the fee account) and confirms them at `POST /v1/zk-checkout/{id}/confirm`. With a `merchantFee`, a third transfer pays it to its wallet, which must hold a confidential account on the token too.

Requires the `payments:write` scope.

**Headers**

- `Idempotency-Key` (string, optional): Optional idempotency key. A retry with the same key and the same request returns the existing payment; the same key with a different request is 409 idempotency_conflict. Takes precedence over body.idempotencyKey.

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

- `amount` (string, required): What the payer pays in total, in XLM (up to 7 decimal places). The merchant receives it minus the 0.18% commission (`netAmount`); the commission (`feeAmount`, rounded up, at least one stroop) goes to Cosmos Pay's fee account. An amount whose net would be zero is refused.
- `recipient` (string, required): The merchant's Stellar account (G...). It must already hold a confidential account on Cosmos Pay's token, registered under auditor id 0; otherwise the payment is refused with `recipient_not_registered`.
- `description` (string, optional): Shown to the payer by the checkout. Not secret, and not on-chain: the public read returns it to anyone holding the payment id.
- `expiresInMinutes` (number, optional): How long the payment stays payable, in minutes. Both transfers must close before it ends.
- `idempotencyKey` (string, optional): Idempotency key. A retry with the same key and the same request returns the existing payment; the same key with a different request is 409 idempotency_conflict. The Idempotency-Key header takes precedence over this field. One is generated when neither is sent.
- `merchantFee` (MerchantFeeDto, optional): Your own fee on this payment, in XLM, paid privately like the rest: a third confidential transfer from the payer to `wallet`, which must already hold a confidential account on Cosmos Pay's token (under auditor id 0) and be neither the recipient nor the fee account. `paidBy: recipient` takes it from `amount` (the merchant receives less); `payer` adds it on top (the payer pays `amount` plus the fee). The 0.18% commission is a share of `amount` either way. Confirm then takes `merchantFeeTxHash` too.

**Responses**

- `201` (`ZkPaymentEntity`): Created
  - `id` (string, required): The private payment id (a cuid).
  - `status` (string, required): As it reads now: a PENDING payment past `expiresAt` reads EXPIRED. It can still settle: transfers that closed before `expiresAt` paid it. One of: `PENDING`, `SUCCEEDED`, `EXPIRED`.
  - `network` (string, required): The network the payment is on, set by the API key that created it. One of: `public`, `testnet`.
  - `tokenContract` (string, required): The confidential token both transfers must be made on.
  - `amount` (string, required): What the payer pays in total, in XLM: `netAmount + feeAmount`, plus `merchantFee.amount` when there is one.
  - `netAmount` (string, required): What the merchant receives: `amount - feeAmount`, less `merchantFee.amount` when there is one.
  - `feeAmount` (string, required): Cosmos Pay's commission, in XLM (decimal string): `feeBps` of the amount the request named (`amount` less a merchant fee with `paidBy: payer`), rounded up to the stroop (0.0000001 XLM) and at least one stroop.
  - `feeBps` (number, required): The commission rate the payment was priced at, in basis points (1 = 0.01%).
  - `recipient` (string, required): The merchant (G...).
  - `feeAccount` (string, required): Cosmos Pay's account the commission (`feeAmount`) is paid to.
  - `payer` (string, required): The account both transfers came from, once settled.
  - `paymentTxHash` (string, required): The transaction of the transfer that paid `netAmount` to the recipient (64 lowercase hex), once settled.
  - `feeTxHash` (string, required): The transaction of the transfer that paid `feeAmount` to the fee account (64 lowercase hex), once settled.
  - `merchantFee` (MerchantFeeEntity, required): The merchant fee the request named, paid by a third confidential transfer to its wallet; null without one.
  - `merchantFeeTxHash` (string, required): The merchant fee's transaction, once settled.
  - `description` (string, required): The description given at create, shown to the payer by the checkout; null when none was given.
  - `idempotencyKey` (string, required): The idempotency key of the create: the one sent (header first, then body), or a generated `zkp_...` key when none was.
  - `expiresAt` (string (date-time), required): When the payment stops being payable (ISO 8601 date-time). Every transfer must close before it.
  - `settledAt` (string (date-time), required): When the later of the two transfers closed, once settled.
  - `createdAt` (string (date-time), required): When the payment was created (ISO 8601 date-time). A transfer that closed more than a minute before it cannot pay it.
  - `updatedAt` (string (date-time), required): When the payment was last changed (ISO 8601 date-time).
- `400` (`ApiErrorBodyEntity`): `invalid_amount`: after the commission (and the merchant fee) the merchant would receive nothing, or the merchant fee rounds to nothing. `recipient_not_registered`: the recipient, or the merchant fee's wallet, has no confidential account on the token under auditor 0. `network_unsupported`: private payments do not run on this network yet (mainnet today; use a dev key).
  - `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`: this key was already used for a different payment. `operation_in_flight`: a concurrent create with the same key changed it while this one ran; retry.
- `429`: RateLimited (see Shared responses)
- `500`: InternalError (see Shared responses)
- `503`: UpstreamUnavailable (see Shared responses)

## Get a private payment by id

`GET /v1/zk-payments/{id}`

One of your private payments: its amounts (`amount`, `netAmount`, `feeAmount`), recipient, payer and the hashes of the transfers that settled it. `status` reads as it is now: a `PENDING` payment past `expiresAt` reads `EXPIRED`, and can still settle with transfers that closed in time. The payer's view of it is the keyless `GET /v1/zk-checkout/{id}`.

Requires the `payments:read` scope.

**Path parameters**

- `id` (string, required)

**Responses**

- `200` (`ZkPaymentEntity`): OK
- `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)

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