# Coinvay Merchant API reference

Version 1.2.0. Interactive page: /docs.html. Download /openapi.json for API clients.

## Settlement and access

Customer invoice payments go directly to the merchant's signed receiving wallet. Merchant plan fees go to the administrator's verified receiving wallets, selected in Admin → Plans & billing. No subscription receiving account is selected by default. The gateway does not custody merchant funds.

Create a key while signed in under Integration. Send Authorization: Bearer YOUR_MERCHANT_KEY from your server. Never embed it in browser bundles, mobile apps, URLs or logs. Your website/app calls your backend; the backend calls the gateway. Features and allowances depend on the current plan configuration.

Coinvay checkout and developer documentation are publicly accessible at https://coinvay.com. Merchant and administrator records require authentication; server integrations use scoped merchant API keys. Do not copy browser cookies into integrations. Cross-origin browser API requests are not a supported integration pattern.

## Quick start

1. Link a receiving wallet and enable desired networks/assets.
2. Create a key in Integration and store it as a backend secret.
3. POST /api/invoices with a trusted order price and a stable, order-specific Idempotency-Key.
4. Persist the returned invoice ID against your order; redirect to checkoutPath resolved against the gateway origin.
5. Verify Paid using GET /api/invoices/{id} or a signed payment.completed webhook. Check the saved invoice/order mapping and currency. invoiceAmount is the original USD order total. Completed-payment webhooks preserve amount as the original order total for existing integrations, with receivedUsd as the actual USD equivalent at the locked quote rate. Payment-list records use amount for the received USD equivalent. Inspect acceptedShortfall, receivedQuantity and shortfallQuantity. Underpaid is not a completed payment; do not fulfil it.
6. Fulfill once using a durable database transaction/outbox. A return URL, supplied hash or pending response is not proof of payment.

## Webhooks

Set a public HTTPS postback URL in Settings. Rotate/save the signing secret once. Return 2xx within eight seconds; delivery redirects are rejected. Events are queued rather than delivered synchronously at settlement. An external authenticated runner is needed for unattended monitoring and delivery retries. It processes up to three eligible deliveries older than one minute per tick, with fewer than six automatic attempts; manual Retry is available. Disabled webhook features pause delivery and do not recreate events omitted while disabled.

X-Gateway-Signature: lowercase HMAC-SHA256 hex over timestamp + '.' + the exact raw request bytes. X-Gateway-Timestamp: Unix seconds. X-Gateway-Event-Id: delivery ID (hook_…), distinct from the signed payload id (pay_…). Enforce a freshness window, use timing-safe comparison, and parse JSON only after verification.

The delivery ID is not covered by the signature. Deduplicate using the signed payment ID and invoice/order identity, not only the header. Commit durable deduplication and fulfillment job creation atomically before acknowledging. For a non-USD store, compare originalAmount/originalCurrency to the trusted local order AND amount/currency to the saved USD invoice total. Never compare USD amount to an INR/AED order total. The updated SDKs perform both checks. Compare the event to the saved order/amount. Download /examples/verify-webhook.mjs for a verifier; your database transaction remains application-specific. Queued retries use the current secret, so coordinate rotation.

## Limits and errors

JSON: 64 KiB. Merchant writes: 180/minute. Checkout: 600/IP/minute plus 90/token/IP/minute. Plans add UTC monthly invoice and active key caps. HTTP 402 indicates a plan restriction, not an unpaid customer invoice. Errors use an error string in JSON. A 429 currently has no Retry-After header. Use bounded exponential backoff and a stable Idempotency-Key for ambiguous creation failures.

## Supported chains

- Bitcoin: BTC; 6 confirmations.
- Tron: TRX, USDT; solidified transactions.
- TON: TON; finalized recipient transactions.
- Ethereum: ETH, DAI, PYUSD, USDC, USDT; 12 confirmations.
- BNB Chain: BNB, USDT, USDC; 15 confirmations.
- Base: ETH, USDC, USDT, DAI; 20 confirmations.
- Solana: SOL, USDT; finalized transaction (native SOL or SPL token transfer).
- Polygon: POL, USDC, PYUSD, USDT, DAI; 128 confirmations.
- Arbitrum: ETH, USDC, USDT; 160 confirmations.
- Optimism: ETH, USDC, USDT; 20 confirmations.
- Avalanche: AVAX, USDC, USDT; 12 confirmations.
- Base Sepolia (testnet): ETH, USDC; 3 confirmations.

The runtime token registry is authoritative. Bitcoin (6 confirmations), Tron TRX/USDT TRC-20 (solidified), and native Gram (GRAM) on TON (finalized recipient transaction) are supported. GRAM is accepted as an alias of the legacy TON asset identifier; stored quotes and API response identifiers stay compatible. Solana supports native SOL and the USDT SPL token (paid to the recipient wallet's token account); other SPL tokens and TON jettons are not supported. Real merchant plan payments reject testnet assets. Query the checkout response options rather than assume every asset is enabled. Merchants add Bitcoin/Tron/TON receiving addresses in Wallets using their current Coinvay password or a fresh confirmation with a linked Google/Apple account, plus enabled MFA. Provider confirmation is single-use, expires after five minutes, and is bound to the exact address and current interactive session; this is account confirmation, not a private-key ownership signature. Outgoing payout verification remains EVM/Solana only.

## Endpoints
### POST /api/connect/requests

Start store account connection. Backend-initiated, ten-minute authorization request. Requires a random installation ID, state and S256 PKCE. Store, callback and webhook must have the same public HTTPS origin. Redirect the administrator to the returned authorizationPath. Signing in alone never issues credentials; explicit interactive authorization is required. See /store-connect.md.

Authentication: checkout capability / no application credential; hosting policy still applies.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Store name, up to 100 characters"
    },
    "storeUrl": {
      "type": "string",
      "description": "Public HTTPS store/backend URL"
    },
    "installation": {
      "type": "string",
      "description": "Stable random installation ID, 32–128 URL-safe characters"
    },
    "redirectUri": {
      "type": "string",
      "description": "Exact HTTPS backend callback"
    },
    "webhookUrl": {
      "type": "string",
      "description": "Optional same-origin HTTPS payment webhook"
    },
    "state": {
      "type": "string",
      "description": "Random state bound to initiating administrator session"
    },
    "codeChallenge": {
      "type": "string",
      "description": "43-character base64url SHA-256 PKCE challenge"
    },
    "codeChallengeMethod": {
      "type": "string",
      "enum": [
        "S256"
      ]
    }
  },
  "required": [
    "name",
    "storeUrl",
    "installation",
    "redirectUri",
    "state",
    "codeChallenge",
    "codeChallengeMethod"
  ],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### GET /api/connect/requests/{requestId}

Review connection request. Nonsecret store origin, requested permissions and callback. Expired or handled requests return 410. Approve or deny through the signed-in Coinvay review screen; a merchant API key cannot grant permission.

Authentication: checkout capability / no application credential; hosting policy still applies.

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/connect/token

Exchange store authorization code. Backend only. Validate state against the initiating session before exchanging the single-use code with its PKCE verifier and exact redirect URI. Returns a store-limited API key and per-store webhook signing secret. Encrypt them locally; never expose or log the response. Existing plan active-key limits apply. Reconnecting the same installation rotates credentials while preserving its store ID.

Authentication: checkout capability / no application credential; hosting policy still applies.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "code": {
      "type": "string",
      "description": "Single-use 64-character code"
    },
    "codeVerifier": {
      "type": "string",
      "description": "Original S256 verifier, 43–128 characters"
    },
    "redirectUri": {
      "type": "string",
      "description": "Exact callback registered at start"
    }
  },
  "required": [
    "code",
    "codeVerifier",
    "redirectUri"
  ],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### GET /api/stores

List connected stores. Owner-level merchant API key or signed-in account required. Store-limited keys cannot list other connections. Responses contain no secrets.

Authentication: merchant bearer key.

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/stores/manual

Register manual store setup. Full merchant API key or signed-in account required. Adds per-store branding and webhook without changing the merchant global settings. Returns a per-store signing secret; store it privately. The manual API key retains its existing full permissions.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Store name"
    },
    "storeUrl": {
      "type": "string",
      "description": "Public HTTPS store/backend URL"
    },
    "installation": {
      "type": "string",
      "description": "Random installation ID"
    },
    "webhookUrl": {
      "type": "string",
      "description": "Optional same-origin HTTPS webhook"
    }
  },
  "required": [
    "name",
    "storeUrl",
    "installation"
  ],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### GET /api/stores/{storeId}

Read owned store. Owner or that store’s scoped key only. Returns nonsecret connection status and branding.

Authentication: merchant bearer key.

Success: HTTP 200. See OpenAPI components for response fields.

### DELETE /api/stores/{storeId}

Disconnect store. Revokes its scoped API keys and cancels queued store webhook deliveries. Existing invoices, payments and receiving wallets are preserved. Disconnecting a manual store does not revoke the merchant-wide key used elsewhere.

Authentication: merchant bearer key.

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/stores/{storeId}/branding

Set store checkout identity. Owner or that store’s scoped key and plan branding feature required. New invoices snapshot businessName/logo. Also accepts multipart/form-data businessName + logo (PNG/JPG/WebP up to 2 MB). Arbitrary logo URLs and SVG uploads are not accepted. Existing invoice identities and other stores remain unchanged.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "businessName": {
      "type": "string",
      "description": "Name shown on this store checkout, up to 100 characters"
    },
    "removeLogo": {
      "type": "boolean"
    }
  },
  "required": [],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### GET /api/currencies

Supported order currencies. Fiat source currencies and ISO decimal precision. No full exchange-rate feed is exposed. Non-USD invoice creation uses a server-fetched Coinbase USD reference, cached for at most five minutes and rounded half up once to USD cents. Rate failure creates no invoice; existing invoices and USD remain payable.

Authentication: checkout capability / no application credential; hosting policy still applies.

Success: HTTP 200. See OpenAPI components for response fields.

### GET /api/health

Service health. Database connectivity and configured network names; separate from provider health.

Authentication: checkout capability / no application credential; hosting policy still applies.

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/invoices

Create an invoice. Creates a merchant-owned invoice. Idempotency-Key is optional, merchant-scoped and persistent; only the first 150 characters are significant. Reusing it returns the original invoice even when the body changes. Use one key per intended order/renewal. Created invoices consume the UTC monthly plan allowance, including cancelled invoices.

Authentication: merchant bearer key.

Request schema:

```json
{
  "$ref": "#/components/schemas/InvoiceInput"
}
```

Success: HTTP 201. See OpenAPI components for response fields.

### GET /api/invoices

List invoices. Up to 1,000 newest invoices for this merchant. No pagination parameters. Use invoice-by-ID for a specific older invoice.

Authentication: merchant bearer key.

Success: HTTP 200. See OpenAPI components for response fields.

### GET /api/invoices/{id}

Get an invoice. Current status, accounting amount and order reference. Missing and other merchants' IDs return 404. Your backend can verify Paid before fulfillment.

Authentication: merchant bearer key.

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/invoices/{id}/cancel

Cancel an invoice. Only New invoices can be cancelled. Sends no refund and moves no crypto.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {},
  "required": [],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### GET /api/checkout/{token}

Read checkout. Checkout capability; no merchant key at application level. Hosting access still applies. Exposes enabled payment options and a public paid receipt, without customer email. Use signed webhooks or authenticated invoice lookup for fulfillment.

Authentication: checkout capability / no application credential; hosting policy still applies.

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/checkout/{token}/quote

Create a payment quote. Locks exact token amount, recipient, asset and network for up to 20 minutes (Bitcoin: 120 minutes), bounded by invoice expiry. A sufficiently fresh matching quote may be reused. Send exactly the quoted amount on the quoted chain. Keep quantity as an integer string. Real merchant plan purchases reject testnet assets.

Authentication: checkout capability / no application credential; hosting policy still applies.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "network": {
      "type": "string",
      "enum": [
        "Bitcoin",
        "Tron",
        "TON",
        "Ethereum",
        "BNB Chain",
        "Base",
        "Solana",
        "Polygon",
        "Arbitrum",
        "Optimism",
        "Avalanche",
        "Base Sepolia"
      ]
    },
    "asset": {
      "type": "string",
      "enum": [
        "ETH",
        "USDC",
        "USDT",
        "BNB",
        "SOL",
        "BTC",
        "TRX",
        "TON",
        "GRAM",
        "DAI",
        "PYUSD",
        "POL"
      ]
    }
  },
  "required": [
    "network",
    "asset"
  ],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/checkout/{token}/verify

Verify transaction evidence. Checks chain, successful canonical transfer, recipient, official asset, time window and confirmations. Merchant invoices accept small shortfalls by default only within both 10 basis points (0.1%) and USD 1 at the locked quote rate. Merchants may disable or adjust limits in Settings. Larger confirmed shortfalls return underpaid=true/status=Underpaid with actual, quoted and shortfall amounts and create no completed payment. Shortfalls ambiguous between invoices require review. Sandbox and Coinvay plan purchases remain exact-amount only. Accepted shortfalls produce Paid with acceptedShortfall=true and retain the actual quantity and invoiceAmount on receipts and signed webhooks. No top-up aggregation is performed. A hash alone is not proof of payment. Pending means wait. Repeated verification of an already-paid invoice may return only paid and a payment ID.

Authentication: checkout capability / no application credential; hosting policy still applies.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "quote": {
      "type": "string",
      "description": "Quote ID."
    },
    "hash": {
      "type": "string",
      "description": "EVM transaction hash, Bitcoin/Tron transaction ID, TON recipient transaction hash, or Solana signature."
    }
  },
  "required": [
    "quote",
    "hash"
  ],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/checkout/{token}/monitor

Monitor a payment quote. Scans for candidate transfers and verifies them. Hosted checkout polls about every 10 seconds. Stop on paid. Do not send new funds after quote expiry; a limited recovery scan may detect timely transfers.

Authentication: checkout capability / no application credential; hosting policy still applies.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "quote": {
      "type": "string",
      "description": "Quote ID."
    }
  },
  "required": [
    "quote"
  ],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### PUT /api/settings

Configure webhook and branding. Update postback URL or business name. A webhook destination must be a public HTTPS hostname, without credentials or a custom port; local hosts and numeric IPs are rejected. Requires the relevant plan feature. Empty postback disables new notifications. Wallet changes require a signed ownership challenge, not this endpoint.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "postback": {
      "type": "string",
      "description": "HTTPS destination, or empty string."
    },
    "businessName": {
      "type": "string",
      "description": "Checkout name.",
      "maxLength": 100
    }
  },
  "required": [],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/settings/webhook-secret

Rotate signing secret. Returns a new webhook secret once. Rotation changes later attempts, including queued retries. Store on your backend separately from the API key; coordinate receiver deployment first. Requires webhooks.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {},
  "required": [],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/webhooks/{id}/retry

Retry a delivery. Requires a merchant-owned delivery ID from bootstrap/hooks and webhook access. HTTP 200 describes the retry operation: inspect ok and error for delivery success. Redirects are not followed.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {},
  "required": [],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/monitor/tick

Run merchant monitoring. Checks up to five pending merchant quotes and eligible webhook deliveries. Does not install a scheduler. An unattended platform runner uses a separate administrator secret.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {},
  "required": [],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### GET /api/bootstrap

Read workspace data. Merchant invoices/payments/payouts (up to 1,000 each), subscriptions (up to 500), API-key metadata, recent webhook metadata (50), public settings, networks, plan and usage. Raw keys/signing secrets are not returned. Prefer invoice-by-ID for status polling.

Authentication: merchant bearer key.

Success: HTTP 200. See OpenAPI components for response fields.

### POST /api/subscriptions/{id}/renew

Create a renewal invoice. Requires an active customer subscription and recurring-invoice feature. Creates/reuses the period's renewal invoice. IDs are in bootstrap/subscriptions. Customers pay each invoice themselves; no automatic withdrawal.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {},
  "required": [],
  "additionalProperties": true
}
```

Success: HTTP 201. See OpenAPI components for response fields.

### POST /api/subscriptions/{id}/cancel

Cancel a customer subscription. Stops a customer recurring-invoice record. Separate from the merchant's Free/Pro/Enterprise membership. No refund is sent.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {},
  "required": [],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### GET /recurring

List automatic subscriptions. Lists this merchant's fixed-token customer authorizations and separate token receipt history. See /automatic-billing.md for deployment and cancellation.

Authentication: merchant bearer key.

Success: HTTP 200. See OpenAPI components for response fields.

### POST /recurring

Create automatic subscription link. Requires the automatic-subscriptions plan feature, a verified receiving wallet and an enabled billing network. First payment is immediate; later cycles are UTC calendar months. Setup requires finite token approval and activation. This is separate from recurring invoices and merchant plan fees.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "description": "Customer-visible title",
      "maxLength": 120
    },
    "network": {
      "type": "string",
      "description": "Configured EVM network",
      "enum": [
        "Ethereum",
        "Base",
        "Polygon",
        "BNB Chain",
        "Base Sepolia"
      ]
    },
    "asset": {
      "type": "string",
      "description": "Supported ERC-20 token",
      "enum": [
        "USDC",
        "USDT",
        "DAI",
        "PYUSD"
      ]
    },
    "amount": {
      "type": "string",
      "description": "Decimal token amount, not USD"
    },
    "maxPayments": {
      "type": "integer",
      "minimum": 1,
      "maximum": 120
    }
  },
  "required": [
    "title",
    "network",
    "asset",
    "amount",
    "maxPayments"
  ],
  "additionalProperties": true
}
```

Success: HTTP 201. See OpenAPI components for response fields.

### POST /recurring/{id}/close

Close unused subscription link. Only unused owner-scoped links can be closed off-chain. Active authorizations require a payer/recipient wallet cancellation.

Authentication: merchant bearer key.

Request schema:

```json
{
  "type": "object",
  "properties": {},
  "required": [],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### GET /recurring-checkout/{token}

Read customer subscription terms. Public capability link. Returns fixed terms, readiness and public on-chain identifiers; no email or secret.

Authentication: checkout capability / no application credential; hosting policy still applies.

Success: HTTP 200. See OpenAPI components for response fields.

### POST /recurring-checkout/{token}/prepare

Prepare wallet authorization or cancellation. Builds transaction calldata only. The customer signs in their wallet. Cancellation requires the payer or merchant recipient wallet and works independently through the contract.

Authentication: checkout capability / no application credential; hosting policy still applies.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "payer": {
      "type": "string",
      "description": "EVM wallet address"
    },
    "action": {
      "type": "string",
      "description": "Omit for activation; cancel for cancellation",
      "enum": [
        "cancel"
      ]
    }
  },
  "required": [
    "payer"
  ],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.

### POST /recurring-checkout/{token}/sync

Verify subscription and collection. Reads chain state at the required confirmation depth and optionally imports a matching Collected event. Pending authorization returns pending:true. Only Confirmed receipt status is payment evidence.

Authentication: checkout capability / no application credential; hosting policy still applies.

Request schema:

```json
{
  "type": "object",
  "properties": {
    "payer": {
      "type": "string",
      "description": "Payer EVM address"
    },
    "tx": {
      "type": "string",
      "description": "Optional collection transaction hash"
    }
  },
  "required": [],
  "additionalProperties": true
}
```

Success: HTTP 200. See OpenAPI components for response fields.
