# CoinsSend Payments: Checkout integration

This context bundle contains selected public documentation. The linked canonical pages remain the source of truth.

Documentation MCP: https://api.coinssend.com/v1/docs/mcp
OpenAPI: https://docs.coinssend.com/openapi.json
Signature fixtures: https://docs.coinssend.com/signature-test-vectors.json

---

Source: https://docs.coinssend.com/QUICKSTART.md

# API Quickstart: invoice → webhook

This quickstart walks through a minimal, end-to-end integration path:

1. Prepare signature-based API key authentication
2. Create an invoice
3. Redirect buyer to the payment page
4. Verify invoice webhooks

***

## Prerequisites

- Base URL: `https://api.coinssend.com`
- A merchant ID from your CoinsSend dashboard
- A merchant API key for signature auth (`Merchant` + `Sign` headers; withdrawals also require `Timestamp`)

References:

- [Authentication](https://docs.coinssend.com/authentication.md)
- [OpenAPI contract](https://docs.coinssend.com/openapi.json)
- [Signature test vectors](https://docs.coinssend.com/signature-test-vectors.json)
- [Invoices](https://docs.coinssend.com/invoices.md)
- [Webhooks](https://docs.coinssend.com/webhooks.md)

> Want a coding assistant to build this flow? Use the full checkout prompt in [AI Integration Prompts](https://docs.coinssend.com/ai-integration-prompts.md) after reviewing these steps.

***

## Step 1) Prepare API key signature auth for merchant endpoints

Get your merchant ID and API key from the CoinsSend dashboard before making public API requests.

For signed endpoints (for example `POST /v1/invoices`), generate:

```text
canonical_body = json_encode(json_decode(request_body)) using PHP defaults
Sign = md5(base64(canonical_body) + API_KEY)
```

Withdrawal requests use a stricter timestamped signature instead:

```text
Timestamp = current Unix timestamp in seconds
Sign = hmac_sha256(base64(canonical_body) + "." + Timestamp, API_KEY)
```

Send `canonical_body` as the request body. PHP's default JSON encoding escapes
slashes and non-ASCII characters, so plain `JSON.stringify()` output is not
always equivalent.

Example in Node.js:

```javascript
import { createHash } from 'node:crypto';

function canonicalJson(value) {
  return JSON.stringify(value)
    .replace(/\//g, '\\/')
    .replace(/[\u0080-\uFFFF]/g, (character) =>
      `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
    );
}

const body = canonicalJson({
  order_id: 'order_10001',
  amount: '100.50',
  is_customer_fee: true,
  allow_card_payments: true,
  success_url: 'https://merchant.example/pay/success',
  cancel_url: 'https://merchant.example/pay/cancel'
});

const sign = createHash('md5')
  .update(Buffer.from(body).toString('base64') + process.env.COINSSEND_API_KEY)
  .digest('hex');
```

Required headers for signed merchant API calls:

- `Merchant: <MERCHANT_ID>`
- `Sign: <computed_md5_signature>`
- `Content-Type: application/json`

For `POST /v1/withdrawals`, send `Timestamp: <unix_timestamp_seconds>` and set `Sign` to the HMAC-SHA256 value.

***

## Step 2) Create an invoice (API key auth)

```bash
BODY='{"order_id":"order_10001","amount":"100.50","is_customer_fee":true,"allow_card_payments":true,"success_url":"https:\/\/merchant.example\/pay\/success","cancel_url":"https:\/\/merchant.example\/pay\/cancel"}'
# BODY is canonical PHP-compatible JSON. Compute SIGN from these exact bytes.
SIGN='<computed_md5_signature>'

curl -X POST "https://api.coinssend.com/v1/invoices" \
  -H "Content-Type: application/json" \
  -H "Merchant: $MERCHANT_ID" \
  -H "Sign: $SIGN" \
  -d "$BODY"
```

Example response:

```json
{
  "status": "success",
  "data": {
    "id": "4e7d786f-a4a6-4a42-b0e5-6f2bb8a5f0f2",
    "order_id": "order_10001",
    "url": "https://pay.coinssend.com/invoice_code",
    "code": "invoice_code",
    "amount": "100.50",
    "payer_amount": "100.50",
    "expired_at": "2026-04-03T12:00:00+00:00",
    "created_at": "2026-04-02T12:00:00+00:00"
  }
}
```

***

## Step 3) Send buyer to payment URL

Use `data.url` from invoice creation response as the checkout URL.

- Typical shape: `https://pay.coinssend.com/{invoice_code}`
- `data.code` can also be used with public invoice lookup: `GET /v1/invoices/{invoiceCode}`
- Direct crypto payment is always available. Card checkout is shown only when `allow_card_payments` is `true` and the invoice amount or remaining amount is at least `$15.00` / `1500` cents.
- `allow_card_payments` and `provider_card` can appear in invoice lookup and payment-page invoice payloads, but they are not part of the `POST /v1/invoices` create response.
- Redirects from success or cancel URLs, provider redirects, provider webhooks, and provider status polling are only UX signals or provider hints. Confirm payment from invoice status changes after chain confirmation, usually through signed webhooks.

***

## Step 4) Handle webhook events

When invoice/payment state changes, CoinsSend sends signed webhook callbacks to your webhook URL.

Common events for this flow:

- `invoice.paid`
- `invoice.expired`
- `aml.rejected.invoice` (when AML blocks invoice deposit)

Verify `X-Signature` using your API key and the **raw** request body bytes:

```php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $payload, $apiKey);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('invalid signature');
}
```

## Next steps

- Expand invoice validation and error handling: [Invoices](https://docs.coinssend.com/invoices.md)
- Add duplicate-safe processing in your webhook consumer: [Webhooks](https://docs.coinssend.com/webhooks.md)
- Do not blindly retry invoice creation: the endpoint has no idempotency-key contract. Reconcile by `order_id` before deciding whether to create again.
- Review request signing details: [Authentication](https://docs.coinssend.com/authentication.md)

---

Source: https://docs.coinssend.com/authentication.md

# Authentication

CoinsSend merchant API endpoints use request signatures. Keep signing code and the API key on a trusted server; never put a merchant API key in browser or mobile code.

Authenticated endpoints require:

| Header      | Required                                             | Description                                                       |
| ----------- | ---------------------------------------------------- | ----------------------------------------------------------------- |
| `Merchant`  | Yes                                                  | Merchant ID                                                       |
| `Sign`      | Yes                                                  | Lowercase hexadecimal request signature                           |
| `Timestamp` | Withdrawals: yes; other merchant endpoints: optional | Unix timestamp in seconds. `X-Timestamp` is accepted as an alias. |

Public catalog and checkout lookup endpoints do not use these headers. See the verified allowlist in [API Reference](https://docs.coinssend.com/api-reference.md) or [OpenAPI](https://docs.coinssend.com/openapi.json).

## Canonical JSON Body

Request signing does **not** use arbitrary raw JSON formatting. For a JSON object or array, the API parses the body and re-encodes it with PHP's default `json_encode` behavior before verifying the signature. That canonical form:

- has no insignificant whitespace;
- preserves object member order;
- escapes `/` as `\/`;
- escapes non-ASCII characters as lowercase `\uXXXX` sequences;
- uses an empty string for a request with no body.

Generate this canonical JSON once, sign it, and send those same bytes as the HTTP body. Keep money values as strings and avoid numeric object keys or language-specific floating-point values.

Interoperability fixtures, including whitespace, URL, Unicode, withdrawal, and empty-GET cases, are in [`signature-test-vectors.json`](https://docs.coinssend.com/signature-test-vectors.json).

### Canonical JSON Helpers

<div datatype="code-tabs">

```php
<?php

function canonicalJson(array $payload): string
{
    return json_encode($payload, JSON_THROW_ON_ERROR);
}
```

```javascript
// Node.js. Object insertion order must match the body you intend to send.
function canonicalJson(payload) {
  return JSON.stringify(payload)
    .replace(/\//g, '\\/')
    .replace(/[\u0080-\uFFFF]/g, character =>
      `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
    )
}
```

```python
import json

def canonical_json(payload):
    encoded = json.dumps(
        payload,
        ensure_ascii=False,
        separators=(',', ':'),
        allow_nan=False,
    )
    escaped = []
    for character in encoded:
        codepoint = ord(character)
        if character == '/':
            escaped.append(r'\/')
        elif codepoint < 0x80:
            escaped.append(character)
        elif codepoint <= 0xFFFF:
            escaped.append(f'\\u{codepoint:04x}')
        else:
            codepoint -= 0x10000
            escaped.append(f'\\u{0xD800 + (codepoint >> 10):04x}')
            escaped.append(f'\\u{0xDC00 + (codepoint & 0x3FF):04x}')
    return ''.join(escaped)
```

</div>

## Legacy MD5 Signature

Invoice creation, wallet creation, balances, and fees accept the legacy signature when no timestamp header is supplied:

```text
canonical_body = canonical_json(request_object) or "" for a bodyless request
payload_base64 = base64_utf8(canonical_body)
Sign = md5(payload_base64 + API_KEY)
```

MD5 here is part of the current compatibility protocol, not a password-hashing recommendation. Always use HTTPS and keep the key server-side.

<div datatype="code-tabs">

```php
<?php

$body = json_encode($payload, JSON_THROW_ON_ERROR);
$sign = md5(base64_encode($body).$apiKey);
```

```javascript
const crypto = require('node:crypto')

const body = canonicalJson(payload)
const payloadBase64 = Buffer.from(body, 'utf8').toString('base64')
const sign = crypto.createHash('md5').update(payloadBase64 + apiKey).digest('hex')
```

```python
import base64
import hashlib

body = canonical_json(payload)
payload_base64 = base64.b64encode(body.encode('utf-8')).decode('ascii')
sign = hashlib.md5((payload_base64 + api_key).encode('utf-8')).hexdigest()
```

</div>

## Timestamped HMAC Signature

`POST /v1/withdrawals` requires a Unix timestamp within 300 seconds of server time. Other authenticated endpoints also accept this mode when `Timestamp` or `X-Timestamp` is supplied.

```text
canonical_body = canonical_json(request_object) or "" for a bodyless request
payload_base64 = base64_utf8(canonical_body)
message = payload_base64 + "." + Timestamp
Sign = hmac_sha256(message, API_KEY)
```

Send exactly the timestamp string used in the signature:

```text
Merchant: 12345678-1234-1234-1234-123456789012
Timestamp: 1716200000
Sign: 64-character-lowercase-hmac-sha256-hex
```

<div datatype="code-tabs">

```php
<?php

$body = json_encode($payload, JSON_THROW_ON_ERROR);
$payloadBase64 = base64_encode($body);
$sign = hash_hmac('sha256', $payloadBase64.'.'.$timestamp, $apiKey);
```

```javascript
const crypto = require('node:crypto')

const body = canonicalJson(payload)
const payloadBase64 = Buffer.from(body, 'utf8').toString('base64')
const sign = crypto
  .createHmac('sha256', apiKey)
  .update(`${payloadBase64}.${timestamp}`)
  .digest('hex')
```

```python
import base64
import hashlib
import hmac

body = canonical_json(payload)
payload_base64 = base64.b64encode(body.encode('utf-8')).decode('ascii')
message = f'{payload_base64}.{timestamp}'
sign = hmac.new(api_key.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest()
```

</div>

The timestamp prevents stale replay but does not make a write idempotent. In particular, it is unsafe to automatically repeat a withdrawal after an unknown outcome.

## Complete Invoice Example

```javascript
const crypto = require('node:crypto')

function canonicalJson(payload) {
  return JSON.stringify(payload)
    .replace(/\//g, '\\/')
    .replace(/[\u0080-\uFFFF]/g, character =>
      `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
    )
}

const payload = {
  order_id: 'order_12345',
  amount: '100.50',
  success_url: 'https://merchant.example/success'
}
const body = canonicalJson(payload)
const payloadBase64 = Buffer.from(body, 'utf8').toString('base64')
const sign = crypto.createHash('md5').update(payloadBase64 + process.env.COINSSEND_API_KEY).digest('hex')

const response = await fetch('https://api.coinssend.com/v1/invoices', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Merchant: process.env.COINSSEND_MERCHANT_ID,
    Sign: sign
  },
  body
})
```

## GET Requests

For an authenticated GET with no body, the canonical body and Base64 payload are both empty. The legacy signature is therefore:

```text
Sign = md5("" + API_KEY)
```

Do not add merchant signature headers to public `GET /v1/invoices/{invoiceCode}`, `GET /v1/get-coin-rate`, or `GET /v1/coins-and-fee` calls.

## Authentication Errors

Authentication failures return a compact body such as:

```json
{
  "error": "Invalid sign"
}
```

| HTTP status | Message                         | Meaning                                               |
| ----------- | ------------------------------- | ----------------------------------------------------- |
| `400`       | `Missing headers`               | `Merchant` or `Sign` is missing                       |
| `400`       | `Missing timestamp`             | Withdrawal timestamp is missing                       |
| `401`       | `Invalid merchant`              | Merchant or API key was not found                     |
| `401`       | `Invalid sign`                  | Signature verification failed                         |
| `401`       | `Timestamp expired`             | Timestamp is invalid or outside the 300-second window |
| `403`       | `Merchant account is suspended` | Merchant is suspended                                 |

These messages are not a separate `code` property. Endpoint and validation
errors use different documented shapes; always branch on the HTTP status and
then parse the applicable schema.

## Operational Guidance

- Keep the API key in a secrets manager or server environment variable.
- Generate and send the body as one immutable string.
- Use a clock synchronized with NTP for withdrawal timestamps.
- Compare webhook HMAC values in constant time; webhook signing is a separate raw-body contract described in [Webhooks](https://docs.coinssend.com/webhooks.md).
- Use the published test vectors in automated client tests before making live calls.

---

Source: https://docs.coinssend.com/invoices.md

# Invoices API

<div class="intro-panel">
  <div class="intro-icon">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect><line x1="1" y1="10" x2="23" y2="10"></line></svg>
  </div>
  <div class="intro-content">
    <p>The Invoices API allows you to create and manage cryptocurrency payment invoices for your customers. Each invoice generates a unique payment page where customers can select their preferred cryptocurrency and complete the payment. Invoices are ideal for e-commerce checkout flows and single-payment scenarios.</p>
  </div>
</div>

## Overview

An invoice is a payment request with a specific amount that customers can pay using various cryptocurrencies. When an invoice is created, customers receive a payment URL where they can choose their preferred cryptocurrency and complete the payment.

### Invoice Lifecycle

<div class="steps-container">
  <div class="step-item">
    <div class="step-number">1</div>
    <div class="step-content">
      <h3>Create Invoice</h3>
      <p>Generate a new invoice with a specified amount using the <code>POST /v1/invoices</code> endpoint.</p>
    </div>
  </div>

  <div class="step-item">
    <div class="step-number">2</div>
    <div class="step-content">
      <h3>Customer Payment</h3>
      <p>Customer visits the payment URL, selects direct crypto payment, or uses card checkout when the invoice allows cards and the remaining amount meets the minimum.</p>
    </div>
  </div>

  <div class="step-item">
    <div class="step-number">3</div>
    <div class="step-content">
      <h3>Payment Confirmation</h3>
      <p>Once payment is confirmed on the blockchain, the invoice status is updated to "paid". Partial payments are supported - if the customer pays less than the full amount, they can complete the payment with a second transaction for the remaining balance.</p>
    </div>
  </div>

  <div class="step-item">
    <div class="step-number">4</div>
    <div class="step-content">
      <h3>Webhook Notification</h3>
      <p>A webhook notification is sent to your server with payment details.</p>
    </div>
  </div>
</div>

## API Endpoints

### Create Invoice

Creates a new invoice for direct cryptocurrency payment, with optional card checkout.

<div class="endpoint-container">
  <div class="endpoint-header">
    <span class="http-method post">POST</span>
    <span class="endpoint-path">/v1/invoices</span>
  </div>

  <div class="endpoint-description">
    <p>This endpoint creates a new invoice with the specified amount and options. The invoice will generate a unique payment page URL that you can redirect your customers to for completing the payment.</p>
  </div>

  <div class="authentication-required">
    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
    Authentication required
  </div>
</div>

#### Request Headers

| Header         | Required | Description                                                                            |
| -------------- | -------- | -------------------------------------------------------------------------------------- |
| `Content-Type` | Yes      | Must be `application/json`                                                             |
| `Merchant`     | Yes      | Your merchant ID                                                                       |
| `Sign`         | Yes      | Request signature (see [Authentication](https://docs.coinssend.com/authentication.md)) |
| `Timestamp`    | No       | Include only when using timestamped HMAC; `X-Timestamp` is accepted as an alias        |

Build the canonical JSON body described in [Authentication](https://docs.coinssend.com/authentication.md),
sign it, and send that same string as the request body.

#### Request Parameters

| Parameter                 | Type    | Required | Default             | Description                                                                                                         |
| ------------------------- | ------- | -------- | ------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `order_id`                | string  | Yes      | -                   | Your order identifier (must be unique per merchant)                                                                 |
| `amount`                  | string  | Yes      | -                   | Invoice amount in USD (minimum `3.00`, e.g., "10.50")                                                               |
| `is_customer_fee`         | boolean | No       | false               | If true, the customer pays the service fee (CoinsSend commission)                                                   |
| `is_customer_network_fee` | boolean | No       | false               | If true, the customer pays the blockchain transaction fee; if false, the merchant covers it                         |
| `allow_card_payments`     | boolean | No       | false               | If true, card checkout may be shown when the invoice amount or remaining amount is at least `$15.00` / `1500` cents |
| `allowed_coins`           | array   | No       | all supported coins | List of currently supported cryptocurrencies to allow for this invoice                                              |
| `coin`                    | string  | No       | -                   | Preselect a currently supported coin for the payment page                                                           |
| `network`                 | string  | No       | -                   | Preselect a network that matches the selected coin                                                                  |
| `success_url`             | string  | No       | -                   | URL to redirect the customer after successful payment                                                               |
| `cancel_url`              | string  | No       | -                   | URL to redirect the customer if they cancel payment                                                                 |

> **Important:** `coin` and `network` must be a currently supported combination.
> Fetch the live catalog from `GET /v1/coins-and-fee`. Requests containing an
> unsupported pair (for example `eth` on `tron`) or unsupported
> `allowed_coins` entries fail validation with HTTP `422`.

See [Supported Coins & Networks](https://docs.coinssend.com/supported-coins.md) for the coin/network
tables and guidance on selecting invoice payment options.

> **Card checkout:** `allow_card_payments` defaults to `false`. Card checkout is not shown or available unless you set `allow_card_payments` to `true` for that invoice. Even when cards are allowed, the invoice amount or remaining amount must be at least `$15.00` / `1500` cents. Direct crypto payment remains available when cards are disabled or the remaining amount is below the card minimum.

#### Request Example

```json
{
  "order_id": "order_12345",
  "amount": "100.50",
  "is_customer_fee": true,
  "is_customer_network_fee": false,
  "allow_card_payments": true,
  "allowed_coins": ["usdt", "eth"],
  "coin": "usdt",
  "network": "tron",
  "success_url": "https://example.com/payment-success",
  "cancel_url": "https://example.com/payment-canceled"
}
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "order_id": "order_12345",
    "url": "https://pay.coinssend.com/invoice_code",
    "code": "invoice_code",
    "amount": "100.50",
    "payer_amount": "100.50",
    "expired_at": "2024-01-02T12:00:00+00:00",
    "created_at": "2024-01-01T12:00:00+00:00"
  }
}
```

#### Response Field Descriptions

| Field          | Type   | Description                                                           |
| -------------- | ------ | --------------------------------------------------------------------- |
| `id`           | string | Unique invoice ID (UUID)                                              |
| `order_id`     | string | Unique order ID in your system                                        |
| `url`          | string | Payment URL for the invoice                                           |
| `code`         | string | Unique invoice code                                                   |
| `amount`       | string | Invoice amount                                                        |
| `payer_amount` | string | Total amount the payer needs to pay including fees                    |
| `expired_at`   | string | ISO 8601 timestamp when the invoice expires (24 hours after creation) |
| `created_at`   | string | ISO 8601 timestamp when the invoice was created                       |

`allow_card_payments` and `provider_card` are not returned by `POST /v1/invoices`. They can appear in invoice lookup and payment-page invoice payloads, where `provider_card` explains whether card checkout is currently available and why it may be disabled.

#### Card Payment Availability

Direct crypto payment is always available on the payment page. Card checkout is optional per invoice and requires both of these conditions:

1. `allow_card_payments` is `true` on the invoice.
2. The invoice amount, or remaining amount after a partial payment, is at least `$15.00` / `1500` cents.

If either condition is not met, the payment page does not show card checkout and customers can still pay directly with crypto. In invoice lookup and payment-page invoice payloads, the `provider_card` object explains the current card state with `enabled`, `disabled_reason`, `minimum_amount_cents`, and `remaining_amount_cents`.

Provider redirects, KYC success, provider webhooks, and provider status polling are not final payment confirmation. Chain confirmation remains the source of truth for moving an invoice to `paid`.

### Create Provider-Card Order

Creates a card checkout order for an existing invoice when `allow_card_payments` is enabled and the remaining invoice amount is at least `$15.00` / `1500` cents.

<div class="endpoint-container">
  <div class="endpoint-header">
    <span class="http-method post">POST</span>
    <span class="endpoint-path">/v1/invoices/{invoiceCode}/provider-card-orders</span>
  </div>
</div>

#### Request Parameters

| Parameter | Type   | Required | Default | Description                                      |
| --------- | ------ | -------- | ------- | ------------------------------------------------ |
| `email`   | string | No       | -       | Optional customer email to store on the invoice. |

Do not send `provider`, `coin`, or `network` fields for card checkout. CoinsSend
selects these values. If a client includes them, they are ignored and cannot
override the selected checkout route.

#### Request Example

```json
{
  "email": "customer@example.com"
}
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "provider_card_order": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "provider": "simpleswap",
      "local_status": "redirect_pending",
      "expires_at": "2026-05-11T12:30:00+00:00",
      "redirect_url": "https://provider.example/checkout/session"
    },
    "reused": false
  }
}
```

Repeating this operation while the same active order is usable returns that
order with `reused: true`. This narrow active-order reuse is not a general
`Idempotency-Key` contract.

### Retrieve Invoice {#retrieve-invoice}

Returns the current public checkout state for an invoice.

```text
GET /v1/invoices/{invoiceCode}
```

No merchant signature is required. The response uses the standard success
envelope with `data.invoice` and current `data.network_fees`. The invoice
object includes amounts, payment selection, status, allowed payment options,
transaction hashes, QR URL, and `provider_card` capability. See the exact
machine-readable schema in [OpenAPI](https://docs.coinssend.com/openapi.json).

Treat the invoice code as a public checkout identifier. Do not use it as
authorization for merchant balances, fees, wallet creation, or withdrawals.

> **AI assistant prompt:** Build a server-side invoice creation and buyer redirect flow.
> Create `POST /v1/invoices`, store the invoice ID, code, status, and URL, then send the buyer to `data.url`.
> Treat success/cancel redirect URLs as UX signals only, not payment confirmation.
> Confirm payment through [Webhooks](https://docs.coinssend.com/webhooks.md), handle `partial` as unpaid progress, and handle `expired` as a terminal failure state.
> For the full checkout prompt, see [AI Integration Prompts](https://docs.coinssend.com/ai-integration-prompts.md).

#### Error Responses

| HTTP status | Response shape                                               | Description                                                                 |
| ----------- | ------------------------------------------------------------ | --------------------------------------------------------------------------- |
| `400`       | `{"error":"Missing headers"}`                                | Merchant or Sign header is missing                                          |
| `401`       | `{"error":"Invalid merchant"}` or `{"error":"Invalid sign"}` | Authentication failed                                                       |
| `403`       | authentication or endpoint error                             | Merchant status blocks invoice creation                                     |
| `409`       | `{"status":"error","message":"Order id already exists"}`     | The merchant order ID already exists; the original response is not replayed |
| `422`       | validation error with `errors`, `status`, and `message`      | Request parameters are invalid                                              |

### Invoice Fees

When an invoice is paid, the amount your customer must send is shown in the
`payer_amount` field. This value includes the blockchain network fee and may also
include the service fee depending on the `is_customer_fee` setting.

1. **Service Fee (CoinsSend commission)** – Calculated as `amount × service_fee_percent`.
   It is added to `payer_amount` only when `is_customer_fee` is `true`. Otherwise
   this fee is deducted from the merchant balance.
2. **Network/Transaction Fee** – Specified in the same coin as the invoice payment.
   Network fee values can be obtained from the `network_fees` list in the
   [Merchant API](https://docs.coinssend.com/merchants.md). It is added to `payer_amount` only when
   `is_customer_network_fee` is `true`. When the merchant covers it (`false`),
   the customer sees no network surcharge and the fee is deducted from the merchant
   proceeds.

The final crypto amount to pay is calculated as:

```text
amount_coin = (amount + (service_fee if is_customer_fee else 0)) / rate
total_coin  = amount_coin + (network_fee if is_customer_network_fee else 0)
```

#### Fee Calculation Examples

**Customer Pays Service Fee**

```
Invoice amount: 100 USD
Network fee: 1 USD
Service fee percent: 0.45%

Service fee: 100 × 0.45% = 0.45 USD
Total amount to pay: 100 + 1 + 0.45 = 101.45 USD
```

**Merchant Pays Service Fee**

```
Invoice amount: 100 USD
Network fee: 1 USD
Service fee percent: 0.45%

Service fee: 100 × 0.45% = 0.45 USD
Total amount to pay: 100 + 1 = 101 USD
Service fee deducted from merchant balance: 0.45 USD
```

**Merchant Pays Network Fee**

```
Invoice amount: 100 USD
Network fee: 1 USD
Service fee percent: 0.45%

Network fee covered by merchant ➜ payer sends: 100.45 USD if customer pays service fee
or 100 USD if merchant also covers service fee.
```

**ETH USDC Example**

Using the ETH network with USDC, the exchange rate on 2025‑07‑09 is
`1 USDC = 1.0000363274333 USD`.

```
Invoice amount: 100 USD
Network fee: 1 USDC
Service fee percent: 0.45%

Service fee: 100 × 0.45% = 0.45 USD
Amount before network fee: 100 + 0.45 = 100.45 USD
Amount in USDC: 100.45 ÷ 1.000036327 ≈ 100.4463293 USDC
Total to send: 100.4463293 + 1 ≈ 101.4463293 USDC
```

The service fee is rounded down to 8 decimal places when converting to
cryptocurrency because invoice amounts are stored in cents. The formula is:

```text
fee_coin = floor(amount_coin × service_fee_percent × 100000000) / 100000000
```

Where `amount_coin` is the invoice amount in the selected coin before the network fee is added.

#### Code Examples

<div datatype="code-tabs">

```php
<?php
// API credentials
$merchantId = 'your_merchant_id';
$apiKey = 'your_api_key';

// Invoice data
$invoiceData = [
    'order_id' => 'order_' . time(),
    'amount' => '100.50',
    'is_customer_fee' => true,
    'allow_card_payments' => true,
    'success_url' => 'https://example.com/payment-success',
    'cancel_url' => 'https://example.com/payment-canceled'
];

// Generate signature
$jsonData = json_encode($invoiceData);
$base64Data = base64_encode($jsonData);
$signature = md5($base64Data . $apiKey);

// Set up request
$ch = curl_init('https://api.coinssend.com/v1/invoices');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Merchant: ' . $merchantId,
    'Sign: ' . $signature
]);

// Execute request
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Process response
if ($httpCode >= 200 && $httpCode < 300) {
    $responseData = json_decode($response, true);
    
    if ($responseData['status'] === 'success') {
        echo "Invoice created successfully!\n";
        echo "Payment URL: " . $responseData['data']['url'] . "\n";
        echo "Invoice Code: " . $responseData['data']['code'] . "\n";
    }
} else {
    echo "Error: " . $response . "\n";
}
```

```javascript
// API credentials
const merchantId = 'your_merchant_id';
const apiKey = 'your_api_key';

// Invoice data
const invoiceData = {
  order_id: 'order_' + Date.now(),
  amount: '100.50',
  is_customer_fee: true,
  allow_card_payments: true,
  success_url: 'https://example.com/payment-success',
  cancel_url: 'https://example.com/payment-canceled'
};

// Generate signature
const jsonData = JSON.stringify(invoiceData)
  .replace(/\//g, '\\/')
  .replace(/[\u0080-\uFFFF]/g, character =>
    `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
  );
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
const crypto = require('node:crypto');
const signature = crypto.createHash('md5').update(base64Data + apiKey).digest('hex');

// Make the API request
fetch('https://api.coinssend.com/v1/invoices', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Merchant': merchantId,
    'Sign': signature
  },
  body: jsonData
})
.then(response => response.json())
.then(data => {
  if (data.status === 'success') {
    console.log("Invoice created successfully!");
    console.log("Payment URL:", data.data.url);
    console.log("Invoice Code:", data.data.code);
  } else {
    console.error("Error:", data);
  }
})
.catch(error => console.error("Request failed:", error));
```

```python
import requests
import json
import base64
import hashlib
import time

# API credentials
merchant_id = 'your_merchant_id'
api_key = 'your_api_key'

# Invoice data
invoice_data = {
    'order_id': f'order_{int(time.time())}',
    'amount': '100.50',
    'is_customer_fee': True,
    'allow_card_payments': True,
    'success_url': 'https://example.com/payment-success',
    'cancel_url': 'https://example.com/payment-canceled'
}

def canonical_json(payload):
    encoded = json.dumps(
        payload, ensure_ascii=False, separators=(',', ':'), allow_nan=False
    )
    escaped = []
    for character in encoded:
        codepoint = ord(character)
        if character == '/':
            escaped.append(r'\/')
        elif codepoint < 0x80:
            escaped.append(character)
        elif codepoint <= 0xFFFF:
            escaped.append(f'\\u{codepoint:04x}')
        else:
            codepoint -= 0x10000
            escaped.append(f'\\u{0xD800 + (codepoint >> 10):04x}')
            escaped.append(f'\\u{0xDC00 + (codepoint & 0x3FF):04x}')
    return ''.join(escaped)


# Generate signature from the exact body that will be sent
json_data = canonical_json(invoice_data)
base64_data = base64.b64encode(json_data.encode()).decode()
signature = hashlib.md5((base64_data + api_key).encode()).hexdigest()

# Set up headers
headers = {
    'Content-Type': 'application/json',
    'Merchant': merchant_id,
    'Sign': signature
}

# Make the API request
response = requests.post(
    'https://api.coinssend.com/v1/invoices', 
    headers=headers, 
    data=json_data
)

# Process response
if response.status_code >= 200 and response.status_code < 300:
    data = response.json()
    
    if data['status'] == 'success':
        print("Invoice created successfully!")
        print(f"Payment URL: {data['data']['url']}")
        print(f"Invoice Code: {data['data']['code']}")
    else:
        print(f"Error: {data}")
else:
    print(f"HTTP Error: {response.status_code}")
    print(f"Response: {response.text}")
```

</div>

## Invoice Statuses

Invoices transition through the following states:

| Status         | Description                                                    |
| -------------- | -------------------------------------------------------------- |
| `new`          | Invoice created; no payment method selected.                   |
| `waiting`      | Payment method locked in; awaiting blockchain payment.         |
| `partial`      | Partial payment detected; remaining balance still due.         |
| `paid`         | Full payment confirmed.                                        |
| `expired`      | Invoice expired (24 hours after creation) before full payment. |
| `aml_rejected` | Payment was rejected by AML processing.                        |

<div class="alert alert-info">
  <div class="alert-icon">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
  </div>
  <div class="alert-content">
    <strong>Note:</strong> Invoice webhook events are currently emitted for <code>invoice.paid</code>, <code>invoice.expired</code>, and AML rejection. Do not assume every intermediate status has an event. See the <a href="https://docs.coinssend.com/webhooks.md">Webhooks documentation</a> for details.
  </div>
</div>

## Best Practices

- **Store Invoice Data**: Always store the invoice code and status in your database for tracking and reconciliation.
- **Handle Webhooks**: Implement webhook handling to receive real-time notifications of payment status changes.
- **Support Partial Payments**: Implement UI and business logic to handle partially paid invoices, allowing customers to complete payments in multiple transactions.
- **Track Payment Progress**: Use `payed_usd`, `payed_crypto`, and payer amount fields from invoice lookup to show payment progress.
- **Use Card Checkout Deliberately**: Set `allow_card_payments` only when you want card checkout available. Keep direct crypto payment available for invoices that do not allow cards or fall below the `$15.00` card minimum after partial payment.
- **Verify Webhooks**: Always verify webhook signatures to ensure they come from CoinsSend.
- **Check Expiration**: Invoices expire after 24 hours. Consider this when implementing payment flows.

## Related Resources

- [Webhooks](https://docs.coinssend.com/webhooks.md) - Receive real-time notifications for invoice status changes
- [Authentication](https://docs.coinssend.com/authentication.md) - Learn about API authentication and signatures
- [Error Handling](https://docs.coinssend.com/error-handling.md) - Understand error responses and how to handle them

## Code Examples

For complete code examples in various languages, see:

- [PHP Examples](https://docs.coinssend.com/examples/php-invoice.md)
- [JavaScript Examples](https://docs.coinssend.com/examples/js-invoice.md)
- [Python Examples](https://docs.coinssend.com/examples/python-invoice.md)

---

Source: https://docs.coinssend.com/webhooks.md

# Webhooks

CoinsSend uses webhooks to notify your application about events that happen in your merchant account. Webhooks are HTTP callbacks that receive notification payloads when events occur.

## Setting Up Webhooks

You can set up a webhook URL in your merchant settings. This URL will receive POST requests when specific events occur.

## Authentication

For security, all webhook requests include an HMAC signature in the `X-Signature` header. You should validate this signature to ensure the webhook was sent by CoinsSend.

The signature is generated using HMAC-SHA256, with your API key as the secret:

```php
// PHP example for validating a webhook signature
function isValidSignature($payload, $signature, $secretKey) {
    $computedSignature = hash_hmac('sha256', $payload, $secretKey);
    return hash_equals($computedSignature, $signature);
}

// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'];
$secretKey = 'your_api_key';

if (!isValidSignature($payload, $signature, $secretKey)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}
```

Important: calculate the HMAC from the exact raw request body bytes that were received. Do not decode the JSON and re-encode it before verification, because key order, spacing, and escaping may change the payload and produce a different signature.

This raw-body rule is specific to **receiving webhooks**. Merchant API request
signing uses the separate canonical-JSON contract in
[Authentication](https://docs.coinssend.com/authentication.md).

> **AI assistant prompt:** Implement a CoinsSend webhook receiver that preserves the raw request body before JSON parsing.
> Read `X-Signature`, verify it with HMAC-SHA256 and the merchant API key, then parse the payload only after the signature passes.
> Add idempotent processing for duplicate deliveries and persist outcomes for `invoice.paid`, `invoice.expired`, and `aml.rejected.invoice`.
> Use [AI Integration Prompts](https://docs.coinssend.com/ai-integration-prompts.md) for the full webhook receiver prompt.

## Webhook Events

The externally emitted `event` values are:

- `invoice.paid`
- `invoice.expired`
- `wallet.transaction`
- `aml.rejected.invoice`
- `aml.rejected.static_wallet`
- `withdrawal.in_progress`
- `withdrawal.success`
- `withdrawal.failed`
- `test`

Use only the public event values listed above when routing webhook payloads.

### Fee Payer Information

For invoice webhooks (`invoice.paid` and `invoice.expired`), the `fee_payer` object indicates who pays each fee:

- `merchant_fee`: `true` if customer pays, `false` if merchant pays
- `network_fee`: `true` if customer pays, `false` if merchant pays

For withdrawal webhooks the boolean direction is different: `true` means the
merchant pays that withdrawal fee. Treat invoice and withdrawal payloads as
event-discriminated schemas rather than sharing one ambiguous `fee_payer` type.

### `invoice.paid`

Sent when an invoice is paid.

```json
{
  "event": "invoice.paid",
  "timestamp": 1746984000,
  "data": {
    "invoice_id": "12345678-1234-1234-1234-123456789012",
    "order_id": "order_12345",
    "code": "invoice_code",
    "status": "paid",
    "amounts": {
      "requested": {
        "amount": "100.50",
        "currency": "usd",
        "crypto": "100.5"
      },
      "paid": {
        "amount": "100.50",
        "currency": "usd",
        "crypto": "100.5"
      }
    },
    "fees": {
      "merchant": {
        "amount": "2.0100",
        "currency": "usd",
        "crypto": "2.0100"
      },
      "network": {
        "amount": "1.4000",
        "currency": "usd",
        "crypto": "1.4000"
      },
      "total": {
        "amount": "3.4100",
        "currency": "usd",
        "crypto": "3.4100"
      }
    },
    "payment": {
      "coin": "usdt",
      "network": "tron",
      "address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
      "rate": "1.0001"
    },
    "fee_payer": {
      "merchant_fee": true,
      "network_fee": true
    },
    "created_at": "2025-01-01T11:30:00Z",
    "paid_at": "2025-01-01T12:00:00Z"
  }
}
```

### `invoice.expired`

Sent when an invoice expires without being paid (after 24 hours).

<div class="alert alert-warning">
  <div class="alert-icon">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
  </div>
  <div class="alert-content">
    <strong>Important:</strong> The <code>payment</code> field is optional and will only be present if a payment method was selected before the invoice expired.
  </div>
</div>

```json
{
  "event": "invoice.expired",
  "timestamp": 1746984000,
  "data": {
    "invoice_id": "12345678-1234-1234-1234-123456789012",
    "order_id": "order_12345",
    "code": "invoice_code",
    "status": "expired",
    "amounts": {
      "requested": {
        "amount": "100.50",
        "currency": "usd",
        "crypto": "100.5"
      },
      "paid": {
        "amount": "0",
        "currency": "usd",
        "crypto": "0"
      }
    },
    "fees": {
      "merchant": {
        "amount": "0",
        "currency": "usd",
        "crypto": "0"
      },
      "network": {
        "amount": "0",
        "currency": "usd",
        "crypto": "0"
      },
      "total": {
        "amount": "0",
        "currency": "usd",
        "crypto": "0"
      }
    },
    "payment": {
      "coin": "usdt",
      "network": "tron",
      "address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
      "rate": "1.0001"
    },
    "fee_payer": {
      "merchant_fee": true,
      "network_fee": true
    },
    "created_at": "2025-01-01T11:30:00Z",
    "expired_at": "2025-01-02T11:30:00Z"
  }
}
```

### `wallet.transaction`

Sent when a static wallet receives a deposit.

```json
{
  "event": "wallet.transaction",
  "timestamp": 1746984120,
  "data": {
    "wallet": {
      "address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
      "network": "tron",
      "coin": "usdt",
      "label": "Main wallet",
      "rate": "1.0001"
    },
    "transaction": {
      "id": "87654321-4321-4321-4321-210987654321",
      "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
      "block_number": 53214321,
      "amounts": {
        "gross": {
          "amount": "10.05",
          "currency": "usd",
          "crypto": "10.05"
        },
        "net": {
          "amount": "9.849",
          "currency": "usd",
          "crypto": "9.849"
        }
      },
      "fees": {
        "merchant": {
          "amount": "0.201",
          "currency": "usd",
          "crypto": "0.201"
        },
        "network": {
          "amount": "0",
          "currency": "usd",
          "crypto": "0"
        },
        "total": {
          "amount": "0.201",
          "currency": "usd",
          "crypto": "0.201"
        }
      },
      "from_address": "TMDKznuDWaZwfZHcM61FYFRx9dAVPndpNv",
      "to_address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
      "status": "success",
      "timestamp": 1746984100
    }
  }
}
```

### `aml.rejected.invoice`

Sent when AML screening blocks an invoice deposit. Snapshot details supplied by
the AML flow are carried inside `risk_details`.

```json
{
  "event": "aml.rejected.invoice",
  "timestamp": 1765472000,
  "data": {
    "wallet_address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
    "coin": "usdt",
    "network": "tron",
    "amounts": {
      "amount": "100.00",
      "currency": "usd",
      "crypto": "1"
    },
    "invoice": {
      "id": "5118a064-d5b9-4fab-ad14-8cf42f0020de",
      "code": "INV123",
      "order_id": "ORDER-1"
    },
    "risk_score": 95,
    "risk_level": "severe",
    "risk_details": {
      "amount_smallest": "1000000",
      "usd_cents": "10000",
      "balance_snapshot": "1000000",
      "source": "misttrack"
    }
  }
}
```

### `aml.rejected.static_wallet`

Sent when AML blocks a static-wallet deposit. Snapshot details supplied by the
AML flow are carried inside `risk_details`.

```json
{
  "event": "aml.rejected.static_wallet",
  "timestamp": 1765472000,
  "data": {
    "wallet_address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
    "coin": "usdt",
    "network": "tron",
    "amounts": {
      "amount": "250.00",
      "currency": "usd",
      "crypto": "2.5"
    },
    "static_wallet": {
      "id": "87654321-4321-4321-4321-210987654321",
      "label": "USDT Payment Address"
    },
    "risk_score": 87,
    "risk_level": "high",
    "risk_details": {
      "amount_smallest": "2500000",
      "usd_cents": "25000",
      "balance_snapshot": "2500000",
      "source": "misttrack"
    }
  }
}
```

### `withdrawal.in_progress`

Sent when a withdrawal request is being processed.

```json
{
  "event": "withdrawal.in_progress",
  "timestamp": 1746984100,
  "data": {
    "withdrawal_id": "5118a064-d5b9-4fab-ad14-8cf42f0020de",
    "status": "processing",
    "amounts": {
      "requested": {
        "amount": "5.0000",
      "currency": "usd",
      "crypto": "5"
      },
      "net": {
        "amount": "5.0000",
        "currency": "usd",
        "crypto": "5"
      }
    },
    "fees": {
      "merchant": {
        "amount": "0.0225",
        "currency": "usd",
        "crypto": "0.0225"
      },
      "network": {
        "amount": "1.4000",
        "currency": "usd",
        "crypto": "1.4"
      },
      "total": {
        "amount": "1.4225",
        "currency": "usd",
        "crypto": "1.4225"
      }
    },
    "payment": {
      "coin": "usdt",
      "network": "tron",
      "to_address": "TLbqtn4EPerCj5a3TmbSffadYgDZ3UEmoe",
      "rate": "1.0001"
    },
    "created_at": "2025-05-11T17:20:50+00:00",
    "updated_at": "2025-05-11T17:21:40+00:00",
    "transaction_hash": "22255482649487ca9f6f70a618c5b2e5e93399c09fd85ab589eff20cb0996e34",
    "fee_payer": {
      "merchant_fee": false,
      "network_fee": false
    }
  }
}
```

### `withdrawal.success`

Sent when a withdrawal has been successfully completed.

```json
{
  "event": "withdrawal.success",
  "timestamp": 1746984108,
  "data": {
    "withdrawal_id": "5118a064-d5b9-4fab-ad14-8cf42f0020de",
    "status": "completed",
    "amounts": {
      "requested": {
        "amount": "5.0000",
        "currency": "usd",
        "crypto": "5"
      },
      "net": {
        "amount": "5.0000",
        "currency": "usd",
        "crypto": "5"
      }
    },
    "fees": {
      "merchant": {
        "amount": "0.0225",
        "currency": "usd",
        "crypto": "0.0225"
      },
      "network": {
        "amount": "1.4000",
        "currency": "usd",
        "crypto": "1.4"
      },
      "total": {
        "amount": "1.4225",
        "currency": "usd",
        "crypto": "1.4225"
      }
    },
    "payment": {
      "coin": "usdt",
      "network": "tron",
      "to_address": "TLbqtn4EPerCj5a3TmbSffadYgDZ3UEmoe",
      "rate": "1.0001"
    },
    "created_at": "2025-05-11T17:20:50+00:00",
    "transaction_hash": "934cc03cfac6a1fe99be9156fde1807cfbe74e1ed7fe5e184596b932b7c9f30a",
    "updated_at": "2025-05-11T17:21:48+00:00",
    "fee_payer": {
      "merchant_fee": false,
      "network_fee": false
    }
  }
}
```

### `withdrawal.failed`

Sent when a withdrawal has failed.

```json
{
  "event": "withdrawal.failed",
  "timestamp": 1746984110,
  "data": {
    "withdrawal_id": "5118a064-d5b9-4fab-ad14-8cf42f0020de",
    "status": "failed",
    "amounts": {
      "requested": {
        "amount": "5.0000",
        "currency": "usd",
        "crypto": "5"
      },
      "net": {
        "amount": "5.0000",
        "currency": "usd",
        "crypto": "5"
      }
    },
    "fees": {
      "merchant": {
        "amount": "0.0225",
        "currency": "usd",
        "crypto": "0.0225"
      },
      "network": {
        "amount": "1.4000",
        "currency": "usd",
        "crypto": "1.4"
      },
      "total": {
        "amount": "1.4225",
        "currency": "usd",
        "crypto": "1.4225"
      }
    },
    "payment": {
      "coin": "usdt",
      "network": "tron",
      "to_address": "TLbqtn4EPerCj5a3TmbSffadYgDZ3UEmoe",
      "rate": "1.0001"
    },
    "created_at": "2025-05-11T17:20:50+00:00",
    "updated_at": "2025-05-11T17:21:50+00:00",
    "error_message": "Insufficient energy on network",
    "fee_payer": {
      "merchant_fee": false,
      "network_fee": false
    }
  }
}
```

## Responding to Webhooks

Your server should respond with a 2xx HTTP status to acknowledge receipt. A
non-2xx response or transport failure can result in another delivery. The exact
attempt count and schedule are not guaranteed, so do not depend on a fixed
retry count.

### Delivery Idempotency

Withdrawal events currently include a stable header:

```text
X-Idempotency-Key: withdrawal:{withdrawal_id}:{event}
```

The same key is reused across withdrawal delivery attempts and manual resends.
Invoice, wallet, AML, and test events do not currently guarantee this header.
For those events, store a receiver-side deduplication key built from the event
and its stable entity identifier where available. Make processing idempotent
even when the header is absent.

## Testing Webhooks

You can test your webhook integration from your merchant dashboard using the "Test Webhook" feature. This will send a test webhook to your configured webhook URL with sample data.

### Test Webhook Payload

The dashboard sends a simple payload when you trigger a test:

```json
{
  "event": "test",
  "timestamp": 1746984000,
  "data": {
    "merchant_id": "12345678-1234-1234-1234-123456789012",
    "test": true,
    "message": "This is a test webhook to verify your integration / - ё"
  }
}
```

## Webhook Delivery

- Webhooks are delivered in real-time as events occur
- Delivery can be attempted more than once; the exact retry policy is not a public constant
- Delivery outcomes are logged for operational review
- Failed webhooks can be manually resent from the merchant dashboard

## Best Practices

1. **Respond Quickly**: Your webhook endpoint should respond as quickly as possible, ideally within a few seconds.

2. **Process Asynchronously**: To ensure quick responses, process the webhook data asynchronously after acknowledging receipt.

3. **Verify Signatures**: Always verify the webhook signature to ensure the request is legitimate.

4. **Handle Duplicate Events**: Use `X-Idempotency-Key` when present and an event/entity deduplication key otherwise.

5. **Monitor Webhook Failures**: Regularly check your merchant dashboard for failed webhook deliveries.

6. **Test Your Integration**: Use the test webhook feature to ensure your endpoint properly handles and processes webhook data.

---

Source: https://docs.coinssend.com/error-handling.md

# Error Handling

CoinsSend does not use one universal error envelope and does not guarantee a
symbolic `code` field. Branch on HTTP status first, then parse the applicable
documented shape.

## Success Envelope

Most successful operations return:

```json
{
  "data": {},
  "status": "success"
}
```

Successful write operations currently return HTTP `200`, including asynchronous
withdrawal submission.

## Error Shapes

### Authentication Errors

Header authentication failures return only `error`:

```json
{
  "error": "Invalid sign"
}
```

Verified messages include `Missing headers`, `Missing timestamp`,
`Invalid merchant`, `Invalid sign`, `Timestamp expired`, and
`Merchant account is suspended`.

### Endpoint Errors

Endpoint failures normally return:

```json
{
  "status": "error",
  "message": "Order id already exists"
}
```

Some endpoints add fields at the top level. Provider-card failures add
`failure_code`; withdrawal failures can add amounts and fee details. Treat
human-readable message text as descriptive rather than a stable machine code.

### Validation

Validation failures use HTTP `422`:

```json
{
  "errors": {
    "amount": [
      "The amount field is required."
    ]
  },
  "status": "error",
  "message": "The given data was invalid."
}
```

### Rate Limiting

The wallet-creation limit returns `status` and `message`. Other HTTP `429`
responses may use a different shape or include `retry_after` and a
`Retry-After` header. Treat rate-limit metadata as optional; see
[Rate Limits and Safe Retries](https://docs.coinssend.com/rate-limits.md).

## HTTP Statuses

| Status | Meaning in the public contract                                                                        |
| ------ | ----------------------------------------------------------------------------------------------------- |
| `400`  | Missing auth input or an endpoint rejection. Withdrawal permission denial currently uses this status. |
| `401`  | Invalid merchant/signature or expired timestamp.                                                      |
| `403`  | Suspended/blocked merchant state or authorization failure.                                            |
| `404`  | Public invoice or QR resource not found.                                                              |
| `409`  | Invoice `order_id` already exists, or wallet creation is already in progress.                         |
| `422`  | Request validation or provider-card capability/state failure.                                         |
| `429`  | A rate limit was exceeded.                                                                            |
| `500`  | Unexpected application failure.                                                                       |
| `502`  | Provider-card checkout could not obtain a usable provider response.                                   |

## Endpoint-Specific Cases

The labels in this table are suggested **client-side categories**. They are not
wire fields returned by CoinsSend.

| Client category                | HTTP status   | Endpoint / condition                                  | Wire shape                                |
| ------------------------------ | ------------- | ----------------------------------------------------- | ----------------------------------------- |
| `missing_headers`              | `400`         | Authenticated endpoint without Merchant or Sign       | `{"error":"Missing headers"}`             |
| `missing_timestamp`            | `400`         | Withdrawal without Timestamp/X-Timestamp              | `{"error":"Missing timestamp"}`           |
| `invalid_signature`            | `401`         | Signature mismatch                                    | `{"error":"Invalid sign"}`                |
| `timestamp_expired`            | `401`         | Invalid or stale timestamp                            | `{"error":"Timestamp expired"}`           |
| `invoice_order_conflict`       | `409`         | Duplicate merchant `order_id`                         | endpoint error                            |
| `validation_failed`            | `422`         | Invalid invoice, wallet, or withdrawal input          | validation error                          |
| `withdrawal_permission_denied` | `400`         | API key lacks withdrawal permission                   | endpoint error                            |
| `withdrawal_rejected`          | `400`         | Balance/fee/service rejection                         | endpoint error with possible extra fields |
| `provider_card_unavailable`    | usually `422` | Current availability or invoice state blocks checkout | endpoint error with `failure_code`        |
| `wallet_creation_in_progress`  | `409`         | Merchant-scoped wallet lock conflict                  | endpoint error                            |

## Robust Parsing

<div datatype="code-tabs">

```php
<?php

function apiErrorMessage(array $body, int $httpStatus): string
{
    return (string) ($body['error'] ?? $body['message'] ?? "HTTP {$httpStatus}");
}

function validationErrors(array $body): array
{
    return is_array($body['errors'] ?? null) ? $body['errors'] : [];
}
```

```javascript
async function parseCoinsSendResponse(response) {
  const body = await response.json()

  if (!response.ok) {
    const error = new Error(body.error || body.message || `HTTP ${response.status}`)
    error.httpStatus = response.status
    error.validationErrors = body.errors || {}
    error.failureCode = body.failure_code
    throw error
  }

  return body.data
}
```

```python
def parse_coinssend_response(response):
    body = response.json()
    if response.ok:
        return body.get('data')

    message = body.get('error') or body.get('message') or f'HTTP {response.status_code}'
    error = RuntimeError(message)
    error.http_status = response.status_code
    error.validation_errors = body.get('errors', {})
    error.failure_code = body.get('failure_code')
    raise error
```

</div>

## Retry Safety

Automatically retry only verified read operations with bounded exponential
backoff and jitter. A timeout on a write does not prove the server rejected it.

- Duplicate invoice `order_id` returns `409`; it does not replay the first
  response.
- Withdrawals have no request idempotency key.
- Static-wallet creation can create another address when repeated.
- Provider-card creation only reuses an already-active order; this is not a
  general idempotency contract.

Reconcile an unknown write outcome instead of blindly resubmitting it. Full
guidance is in [Rate Limits and Safe Retries](https://docs.coinssend.com/rate-limits.md).

## Troubleshooting Invalid Signatures

- Generate the PHP-compatible canonical JSON described in
  [Authentication](https://docs.coinssend.com/authentication.md).
- Sign and send the same immutable canonical string.
- Use strings for monetary amounts.
- For withdrawals, recompute a fresh timestamp and HMAC.
- Run the client against
  [`signature-test-vectors.json`](https://docs.coinssend.com/signature-test-vectors.json), including the
  URL/Unicode vector.
- Remember that webhook verification is different: it uses the exact raw
  received body bytes.

The exact machine-readable response schemas are in
[OpenAPI](https://docs.coinssend.com/openapi.json).

---

Source: https://docs.coinssend.com/rate-limits.md

# Rate Limits and Safe Retries

The public contract does not promise universal `X-RateLimit-*` headers or one
fixed global request count. Clients must handle HTTP `429` on any operation.

## Documented Wallet-Creation Limit

`POST /v1/wallet-address` has this merchant limit:

- threshold: 100 successful wallet creations;
- window: 60 seconds;
- key: current merchant;
- failed creation attempts are not counted;
- rejection: HTTP `429` with `status` and `message`.

Example:

```json
{
  "status": "error",
  "message": "Too many requests. Please try again later."
}
```

This response does not currently guarantee
`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`,
`Retry-After`, or `retry_after`.

## Additional Limits

Additional limits may apply. Their response bodies and headers are not part of
a universal contract. Clients should tolerate HTTP `429` whether or not
`Retry-After` is present.

## Retry Decision Table

| Operation                                              | Automatic retry after `429` or transport failure? | Reason                                                                                 |
| ------------------------------------------------------ | ------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `GET /v1/get-coin-rate`                                | Yes, with bounded backoff                         | Read-only public discovery                                                             |
| `GET /v1/coins-and-fee`                                | Yes, with bounded backoff and caching             | Read-only public discovery                                                             |
| `GET /v1/merchants/balances`                           | Yes, with a fresh signature/timestamp when used   | Read-only merchant state                                                               |
| `GET /v1/merchants/fees`                               | Yes, with a fresh signature/timestamp when used   | Read-only merchant state                                                               |
| `GET /v1/invoices/{invoiceCode}`                       | Yes                                               | Read-only checkout state                                                               |
| `GET /v1/wallet-addresses/{walletAddress}/qr`          | Yes                                               | Read-only binary response                                                              |
| `POST /v1/invoices`                                    | No blind retry after an unknown outcome           | `order_id` uniqueness returns `409`; it does not replay the original response          |
| `POST /v1/withdrawals`                                 | No                                                | No request idempotency key; timestamp freshness is not idempotency                     |
| `POST /v1/wallet-address`                              | No for static wallets                             | A successful retry can create another address                                          |
| `POST /v1/invoices/{invoiceCode}/provider-card-orders` | Do not generalize                                 | Only an already-active order is reused; there is no general `Idempotency-Key` contract |

An HTTP response proves whether a request was rejected. A client-side timeout
does not: the server may already have accepted the write.

## Backoff for Safe Reads

For read operations, use bounded exponential backoff with jitter:

```javascript
async function retrySafeRead(makeRequest, maxAttempts = 4) {
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    const response = await makeRequest()

    if (response.status !== 429 || attempt === maxAttempts) {
      return response
    }

    const retryAfterHeader = response.headers.get('Retry-After')
    const retryAfter = retryAfterHeader === null ? Number.NaN : Number(retryAfterHeader)
    const baseDelayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1000
      : Math.min(500 * 2 ** (attempt - 1), 8000)
    const jitterMs = Math.floor(Math.random() * 250)

    await new Promise(resolve => setTimeout(resolve, baseDelayMs + jitterMs))
  }
}
```

For timestamp-signed reads, recompute `Timestamp` and `Sign` for each attempt.
Never sleep and reuse a timestamp that can age beyond the 300-second signature
window.

## Handling an Unknown Write Outcome

1. Persist the client order/reference and request intent before sending.
2. If a complete success response arrives, persist the returned CoinsSend ID.
3. If the result is unknown, do not issue a blind duplicate.
4. Reconcile using a known returned/public identifier where one exists, or
   investigate through merchant operations/support.
5. For withdrawals, wait for the signed withdrawal webhook when an ID was
   obtained; otherwise escalate the unknown outcome instead of resubmitting.

## Monitoring

- Record endpoint, HTTP status, request correlation data, and `Retry-After` when
  present; never log the API key or full `Sign` header.
- Separate application `429` responses from edge `429` responses.
- Alert on sustained rate limiting and unknown outcomes for write operations.
- Cache the catalog according to its advertised 900-second cache lifetime.

For request-signature freshness and canonicalization, see
[Authentication](https://docs.coinssend.com/authentication.md). For exact operation metadata, see
[OpenAPI](https://docs.coinssend.com/openapi.json).
