# Trustly Pay API Reference

> Version: v1 | Last updated: 2026-09-05

---

## Table of Contents

- [Overview](#overview)
- [Base URL](#base-url)
- [Authentication](#authentication)
- [API Keys](#api-keys)
- [Test Mode vs Live Mode](#test-mode-vs-live-mode)
- [Response Format](#response-format)
- [Headers](#headers)
- [Pagination](#pagination)
- [Idempotency](#idempotency)
- [Rate Limits](#rate-limits)
- [Payment State Machine](#payment-state-machine)
- [Endpoints](#endpoints)
  - [Payments](#payments-1)
  - [Payment Status & Links](#payment-status--links)
  - [Crypto Invoices](#crypto-invoices)
  - [Exchange Rates](#exchange-rates)
  - [Checkout](#checkout-1)
  - [Customers](#customers-1)
  - [Refunds](#refunds-1)
  - [Payouts](#payouts-1)
  - [Balance](#balance-1)
  - [Webhooks](#webhooks-1)
  - [Projects](#projects-1)
  - [Payment Methods](#payment-methods-1)
  - [Limits](#limits-1)
  - [Health](#health-1)
  - [OpenAPI](#openapi-1)
  - [Merchant Portal API](#merchant-portal-api)
- [Webhooks](#webhooks)
- [Error Reference](#error-reference)
- [Security](#security)
- [SDKs](#sdks)
- [Telegram Integration](#telegram-integration)
- [Website Integration](#website-integration)
- [AI Integration](#ai-integration)
- [Changelog](#changelog)

---

## Overview

Trustly Pay is a payment processing platform providing a unified API for accepting payments via card, SBP (Система Быстрых Платежей), payment links, and cryptocurrency. The API follows RESTful conventions, uses JSON for request/response bodies, and returns standard HTTP status codes.

All endpoints are namespaced under `/api/v1/`.

---

## Base URL

| Environment | Base URL |
|-------------|----------|
| Production  | `https://api.trustlypay.io` |
| Sandbox     | `https://sandbox-api.trustlypay.io` |

All endpoints in this reference are relative to the base URL:

```
https://api.trustlypay.io/api/v1/...
```

---

## Authentication

All authenticated endpoints require an API key passed via one of two methods:

### Method 1: Bearer Token

```http
Authorization: Bearer op_test_xxxxxxxxxxxxxxxx
```

### Method 2: X-API-Key Header

```http
X-API-Key: op_test_xxxxxxxxxxxxxxxx
```

Both methods are equivalent. Use whichever your HTTP client supports more conveniently.

### Permissions by Scope

| Scope | Access |
|-------|--------|
| `payments:write` | Create, cancel payments |
| `payments:read` | List, get payments |
| `customers:write` | Create, update, delete customers |
| `customers:read` | List, get customers |
| `refunds:write` | Create refunds |
| `refunds:read` | List, get refunds |
| `payouts:write` | Create payouts |
| `payouts:read` | List payouts |
| `balance:read` | View balance and transactions |
| `webhooks:write` | Create, update, delete webhook endpoints |
| `webhooks:read` | List, get webhook endpoints |

---

## API Keys

Keys follow the format:

| Prefix | Environment |
|--------|-------------|
| `op_test_` | Sandbox/Test |
| `op_live_` | Production/Live |

Example: `op_test_a1b2c3d4e5f6g7h8`

Webhook secrets use the prefix `whsec_`.

---

## Test Mode vs Live Mode

| Property | Test Mode | Live Mode |
|----------|-----------|-----------|
| Key prefix | `op_test_` | `op_live_` |
| Base URL | `sandbox-api.trustlypay.io` | `api.trustlypay.io` |
| Transactions | Simulated, no real money | Real transactions |
| Test cards | Supported | N/A |
| Webhooks | Fire to registered URLs | Fire to registered URLs |
| Rate limits | Same as production | 100 req/60s |

---

## Response Format

### Success (single resource)

```json
{
  "data": { ... }
}
```

### Success (list)

```json
{
  "data": [ ... ],
  "has_more": true,
  "next_cursor": "pay_1704067200_a1b2c3d4"
}
```

### Error

```json
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "The 'amount' field must be a positive integer.",
    "request_id": "req_1704067200_a1b2c3d4",
    "details": {
      "field": "amount",
      "received": -5
    }
  }
}
```

---

## Headers

### Response Headers

| Header | Description |
|--------|-------------|
| `X-Request-Id` | Unique request identifier (`req_<timestamp>_<hex8>`) |
| `X-RateLimit-Limit` | Max requests per window (default: `100`) |
| `X-RateLimit-Remaining` | Remaining requests in current window |
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
| `Retry-After` | Seconds until retry is allowed (on `429` only) |
| `Idempotent-Replayed` | `true` if response is a replay of a previous request |

### Request Headers

| Header | Required | Description |
|--------|----------|-------------|
| `Authorization` | Conditional | `Bearer <API_KEY>` (alternative to `X-API-Key`) |
| `X-API-Key` | Conditional | `<API_KEY>` (alternative to `Authorization`) |
| `Idempotency-Key` | Optional | Unique key for idempotent requests |
| `Content-Type` | Yes (POST/PATCH) | `application/json` |

---

## Pagination

All list endpoints support cursor-based pagination.

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | `20` | Number of results (1–100) |
| `starting_after` | string | — | Return items after this ID |

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `has_more` | boolean | `true` if more results exist |
| `next_cursor` | string | Pass as `starting_after` to get next page |

### Example

```bash
# First page
curl -H "X-API-Key: op_test_xxx" \
  "https://api.trustlypay.io/api/v1/payments?limit=50"

# Second page
curl -H "X-API-Key: op_test_xxx" \
  "https://api.trustlypay.io/api/v1/payments?limit=50&starting_after=pay_1704067200_a1b2c3d4"
```

---

## Idempotency

Idempotency is supported on all `POST` endpoints via the `Idempotency-Key` header.

| Rule | Behavior |
|------|----------|
| Same key + same body | Replays stored 2xx response, sets `Idempotent-Replayed: true` |
| Same key + different body | Returns `409 IDEMPOTENCY_CONFLICT` |
| TTL | 24 hours |
| Storage | Only `2xx` responses are stored |
| Methods | `POST` only |

### Example

```bash
curl -X POST "https://api.trustlypay.io/api/v1/payments" \
  -H "X-API-Key: op_test_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: idem_order_12345" \
  -d '{"amount": 1000, "currency": "RUB"}'
```

On network timeout, retry with the **same** `Idempotency-Key`. If the first request succeeded, you will receive the same response without creating a duplicate.

---

## Rate Limits

| Limit | Value |
|-------|-------|
| Requests per window | 100 |
| Window duration | 60 seconds |
| Algorithm | Token bucket |

When exceeded, the API returns `429 RATE_LIMITED` with a `Retry-After` header indicating the number of seconds to wait.

---

## Payment State Machine

```
                  ┌─────────────────────────────────────────┐
                  │                 created                 │
                  └────┬──────────┬───────────┬─────────────┘
                       │          │           │
                       ▼          │           ▼
                  ┌────────┐      │     ┌─────────┐
                  │pending │      │     │ expired │
                  └───┬────┘      │     └─────────┘
                      │           │          │
           ┌──────┬──┴───┬───┐   │          │
           ▼      ▼      ▼   ▼   │          │
       ┌────────┐ ┌───┐ ┌───┐ ┌──┤          │
       │succeed-│ │   │ │   │ │  ▼          │
       │   ed   │ │   │ │   │ │ canceled    │
       └───┬────┘ │   │ │   │ │             │
           │      │   │ │   │ │             │
           ▼      │   │ │   │ │             │
     ┌──────────┐ │   │ │   │ │             │
     │refunded/ │ │   │ │   │ │             │
     │partially │ │   │ │   │ │             │
     │_refunded │ │   │ │   │ │             │
     └──────────┘ │   │ │   │ │             │
                  │   │ │   │ │             │
                  ▼   │ ▼   │ │             │
             ┌─────┐  │┌───┐│ │             │
             │fail-│  ││   ││ │             │
             │ ed  │  ││   ││ │             │
             └─────┘  │└───┘│ │             │
                      │     │ │             │
                      ▼     ▼ ▼             ▼
                 (terminal states: failed, canceled, expired, refunded)
```

### State Transitions

| From | To |
|------|----|
| `created` | `pending`, `canceled`, `expired` |
| `pending` | `succeeded`, `failed`, `canceled`, `expired` |
| `succeeded` | `refunded`, `partially_refunded` |
| `partially_refunded` | `refunded` |
| `failed` | — (terminal) |
| `canceled` | — (terminal) |
| `expired` | — (terminal) |
| `refunded` | — (terminal) |

---

## Endpoints

---

### Payments

---

#### POST /api/v1/payments — Create Payment

Create a new payment intent. Use the returned `checkout_url` to redirect the customer, or collect payment details server-side.

**Auth:** `payments:write`
**Idempotent:** Yes (use `Idempotency-Key` header)

##### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `amount` | integer | Yes | Amount in kopecks/cents (1–99,999,999) |
| `currency` | string | Yes | ISO 4217 uppercase (`RUB`, `USD`, `EUR`) |
| `description` | string | No | Payment description (max 500 chars) |
| `order_id` | string | No | Merchant's internal order ID |
| `payment_method` | string | No | `"sbp"`, `"card"`, `"link"`, `"crypto"` (default: `"card"`) |
| `crypto_asset` | string | No | Crypto asset (`"USDT"`, `"TON"`, `"BTC"`, `"ETH"`, `"TRX"`). Used when payment_method is `"crypto"` |
| `crypto_network` | string | No | Blockchain network (`"TRON"`, `"TON"`, `"BITCOIN"`, `"ETHEREUM"`). Used when payment_method is `"crypto"` |
| `memo` | string | No | Optional crypto memo/tag |
| `customer_id` | string | No | Associated customer ID (`cust_*`) |
| `return_url` | string | No | Redirect URL after successful payment |
| `cancel_url` | string | No | Redirect URL after payment cancellation |
| `webhook_url` | string | No | Override webhook URL for this payment |
| `metadata` | object | No | Key-value pairs (max 20 keys, values max 500 chars) |
| `expires_in` | integer | No | Seconds until payment expires (default: 3600) |

##### Request Example

```bash
curl -X POST "https://api.trustlypay.io/api/v1/payments" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_12345_abc" \
  -d '{
    "amount": 150000,
    "currency": "RUB",
    "description": "Order #12345",
    "order_id": "12345",
    "payment_method": "card",
    "return_url": "https://example.com/success",
    "cancel_url": "https://example.com/cancel",
    "metadata": {
      "order_id": "12345",
      "source": "website"
    },
    "expires_in": 1800
  }'
```

##### Response (201 Created)

```json
{
  "data": {
    "id": "pay_1704067200_a1b2c3d4",
    "object": "payment",
    "merchant_id": "merch_xxx",
    "project_id": "prj_001",
    "customer_id": null,
    "external_id": null,
    "order_id": "12345",
    "amount": 150000,
    "currency": "RUB",
    "description": "Order #12345",
    "status": "created",
    "payment_method": "card",
    "provider": "stripe",
    "provider_transaction_id": null,
    "checkout_url": "https://checkout.trustlypay.io/pay/ct_a1b2c3d4e5f6g7h8",
    "return_url": "https://example.com/success",
    "cancel_url": "https://example.com/cancel",
    "metadata": {
      "order_id": "12345",
      "source": "website"
    },
    "fee": null,
    "net_amount": null,
    "expires_at": "2026-09-05T14:00:00Z",
    "created_at": "2026-09-05T13:00:00Z",
    "updated_at": "2026-09-05T13:00:00Z",
    "paid_at": null,
    "canceled_at": null,
    "failed_at": null
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 400 | `INVALID_REQUEST` | Missing required fields or invalid types |
| 400 | `AMOUNT_TOO_SMALL` | Amount < 1 |
| 400 | `AMOUNT_TOO_LARGE` | Amount > 99,999,999 |
| 400 | `INVALID_CURRENCY` | Not uppercase ISO 4217 |
| 400 | `PAYMENT_METHOD_DISABLED` | Method not enabled for merchant |
| 400 | `PAYMENT_LIMIT_EXCEEDED` | Amount outside allowed limits |
| 403 | `MERCHANT_SUSPENDED` | Merchant account suspended |
| 403 | `PAYMENTS_SUSPENDED` | Payments disabled for project |
| 409 | `IDEMPOTENCY_CONFLICT` | Same key, different body |
| 429 | `RATE_LIMITED` | Rate limit exceeded |

---

#### GET /api/v1/payments — List Payments

Retrieve a paginated list of payments.

**Auth:** `payments:read`

##### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `status` | string | — | Filter by status |
| `payment_method` | string | — | Filter by method (`card`, `sbp`, `link`, `crypto`) |
| `order_id` | string | — | Filter by merchant order ID |
| `limit` | integer | `20` | Results per page (1–100) |
| `starting_after` | string | — | Cursor for pagination |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/payments?status=succeeded&limit=10"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "pay_1704067200_a1b2c3d4",
      "object": "payment",
      "merchant_id": "merch_xxx",
      "project_id": "prj_001",
      "amount": 150000,
      "currency": "RUB",
      "status": "succeeded",
      "payment_method": "card",
      "created_at": "2026-09-05T13:00:00Z",
      "paid_at": "2026-09-05T13:01:22Z"
    }
  ],
  "has_more": true,
  "next_cursor": "pay_1704067200_a1b2c3d4"
}
```

---

#### GET /api/v1/payments/{id} — Get Payment

Retrieve details of a single payment.

**Auth:** Any valid API key

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Payment ID (`pay_*`) |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/payments/pay_1704067200_a1b2c3d4"
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "pay_1704067200_a1b2c3d4",
    "object": "payment",
    "merchant_id": "merch_xxx",
    "project_id": "prj_001",
    "customer_id": null,
    "order_id": "12345",
    "amount": 150000,
    "currency": "RUB",
    "description": "Order #12345",
    "status": "succeeded",
    "payment_method": "card",
    "provider": "stripe",
    "provider_transaction_id": "txn_xxx",
    "checkout_url": null,
    "return_url": "https://example.com/success",
    "fee": 4500,
    "net_amount": 145500,
    "expires_at": null,
    "created_at": "2026-09-05T13:00:00Z",
    "updated_at": "2026-09-05T13:01:22Z",
    "paid_at": "2026-09-05T13:01:22Z",
    "canceled_at": null,
    "failed_at": null
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 404 | `PAYMENT_NOT_FOUND` | Payment does not exist |

---

#### POST /api/v1/payments/{id} — Cancel Payment

Cancel a payment that has not yet been paid. Only payments in `created` or `pending` status can be canceled.

**Auth:** `payments:write`

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Payment ID (`pay_*`) |

##### Request Body

Empty object `{}`

##### Request Example

```bash
curl -X POST "https://api.trustlypay.io/api/v1/payments/pay_1704067200_a1b2c3d4" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{}'
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "pay_1704067200_a1b2c3d4",
    "object": "payment",
    "status": "canceled",
    "canceled_at": "2026-09-05T13:05:00Z",
    "updated_at": "2026-09-05T13:05:00Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 400 | `INVALID_REQUEST` | Payment in non-cancellable status (`succeeded`, `failed`, `canceled`, `expired`, `refunded`) |
| 404 | `PAYMENT_NOT_FOUND` | Payment does not exist |

##### Webhook Effects

Fires `payment.canceled`.

---

### Payment Status & Links

---

#### GET /api/v1/payments/status/{id} — Public Payment Status Check

Quick real-time status check for a payment. Designed for payment widgets, checkouts, and customer polling without exposing secret API keys. Automatically checks live status from upstream banking providers (e.g., Platega) if payment is pending.

**Auth:** None (Public)

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Payment ID (`pay_*`) |

##### Request Example

```bash
curl "https://api.trustlypay.io/api/v1/payments/status/pay_1704067200_a1b2c3d4"
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "pay_1704067200_a1b2c3d4",
    "status": "succeeded",
    "amount": 150000,
    "currency": "RUB",
    "paid_at": "2026-09-05T13:05:00.000Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 400 | `INVALID_REQUEST` | ID is missing or empty |
| 404 | `NOT_FOUND` | Payment does not exist |

---

#### GET /api/v1/payment-links/{token} — Get Payment Link Details

Retrieves payment link information, available payment methods, fee distribution, and merchant details for checkout rendering.

**Auth:** None (Public)

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `token` | string | Link token (`pl_*`) |

##### Request Example

```bash
curl "https://api.trustlypay.io/api/v1/payment-links/pl_acc764b507f74d97a8191fce2012e3bb"
```

##### Response (200 OK)

```json
{
  "data": {
    "token": "pl_acc764b507f74d97a8191fce2012e3bb",
    "name": "Invoice #1042",
    "description": "Payment for digital services",
    "amount": 10000,
    "currency": "RUB",
    "merchant_name": "Acme Corp",
    "merchant_avatar_url": null,
    "fee_payer": "customer",
    "commission_rate": 5.0,
    "expires_at": "2026-09-05T13:30:00.000Z",
    "return_url": "https://merchant.site/success",
    "payment_methods": [
      { "method": "sbp", "name": "Система быстрых платежей", "description": "Оплата по QR-коду СБП", "available": true },
      { "method": "card", "name": "Банковская карта", "description": "Visa, Mastercard, МИР", "available": true },
      { "method": "crypto", "name": "Криптовалюта", "description": "USDT, TON, BTC, ETH, TRX", "available": true }
    ],
    "support": {
      "telegram": "https://t.me/trustly_support",
      "email": "support@trustlypay.io",
      "workHours": "24/7/365"
    }
  }
}
```

---

#### POST /api/v1/payment-links/{token} — Pay via Payment Link

Initiates payment via a specific method (card, sbp, or crypto) for a payment link.

**Auth:** None (Public)

##### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `method` | string | Yes | `"sbp"`, `"card"`, or `"crypto"` |
| `crypto_asset` | string | No | `"USDT"`, `"TON"`, `"BTC"`, `"ETH"`, `"TRX"` (required if `method` is crypto) |
| `crypto_network` | string | No | `"TRON"`, `"TON"`, `"BITCOIN"`, `"ETHEREUM"` (required if `method` is crypto) |

##### Request Example (SBP)

```bash
curl -X POST "https://api.trustlypay.io/api/v1/payment-links/pl_acc764b507f74d97a8191fce2012e3bb"   -H "Content-Type: application/json"   -d '{ "method": "sbp" }'
```

##### Response (200 OK — SBP)

```json
{
  "data": {
    "payment_id": "pay_1704067200_a1b2c3d4",
    "status": "pending",
    "is_test": false,
    "checkout_url": "https://qr.nspk.ru/...",
    "sbp_payload": {
      "qr_data": "https://qr.nspk.ru/...",
      "deep_link": "https://qr.nspk.ru/...",
      "is_direct_h2h": true,
      "expires_in": "30:00"
    },
    "message": "Платёж СБП сформирован. Отсканируйте QR-код в приложении вашего банка."
  }
}
```

---

### Crypto Invoices

---

#### GET /api/v1/crypto/invoice/{id} — Get Crypto Invoice Details

Retrieves details of a generated cryptocurrency payment invoice, destination address, required network, exchange rate, and incoming blockchain transactions with confirmation status.

**Auth:** None (Public)

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Crypto invoice ID (`cinv_*`) |

##### Request Example

```bash
curl "https://api.trustlypay.io/api/v1/crypto/invoice/cinv_1704067200_a1b2"
```

##### Response (200 OK)

```json
{
  "data": {
    "invoice": {
      "id": "cinv_1704067200_a1b2",
      "paymentId": "pay_1704067200_a1b2c3d4",
      "asset": "USDT",
      "network": "TRON",
      "protocol": "TRC20",
      "expectedAmount": "10000000",
      "destination": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
      "exchangeRate": 91.5,
      "fiatAmount": 915,
      "fiatCurrency": "RUB",
      "cryptoAmountHuman": 10.0,
      "expiresAt": "2026-09-05T13:30:00.000Z",
      "status": "pending",
      "memo": null,
      "merchantName": "Acme Corp",
      "receivedAmount": "0",
      "explorerAddressUrl": "https://tronscan.org/#/address/TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
    },
    "payment": {
      "id": "pay_1704067200_a1b2c3d4",
      "status": "pending",
      "amount": 915,
      "currency": "RUB",
      "description": "Order #12345",
      "orderId": "12345"
    },
    "transactions": []
  }
}
```

---

#### GET /api/v1/crypto/invoice/{id}/tx — Explorer Redirect

Redirects (HTTP 302) directly to the blockchain explorer for confirmed or first detected transaction of this crypto invoice.

**Auth:** None (Public)

---

### Exchange Rates

---

#### GET /api/rates — Live Crypto & Fiat Exchange Rates

Returns live cryptocurrency exchange rates in RUB (USDT, TON, BTC) aggregated from Binance, CoinGecko, and CBR, cached for 30 seconds.

**Auth:** None (Public)

##### Request Example

```bash
curl "https://api.trustlypay.io/api/rates"
```

##### Response (200 OK)

```json
{
  "success": true,
  "rates": {
    "USDT": 91.5,
    "TON": 485.0,
    "BTC": 7500000.0
  },
  "updatedAt": "2026-10-09T05:00:00.000Z"
}
```

---

### Internal & Provider Webhooks

---

#### POST /api/v1/webhooks/providers/platega — Platega Provider Callback

Handles direct payment confirmation notifications from Platega payment provider.

**Auth:** Provider headers (`X-MerchantId`, `X-Secret`)

##### Request Headers

| Header | Description |
|--------|-------------|
| `X-MerchantId` | Platega Merchant UUID |
| `X-Secret` | Platega Secret API Key |

##### Request Body

```json
{
  "id": "ad7a812a-0000-0000-0000-000000000000",
  "amount": 150.00,
  "currency": "RUB",
  "status": "CONFIRMED",
  "paymentMethod": 2,
  "payload": "pay_1704067200_a1b2c3d4"
}
```

---

#### POST /api/v1/webhooks/deliver — Webhook Delivery Engine (Cron Worker)

Internal worker endpoint that processes scheduled retry deliveries for registered merchant webhooks.

**Auth:** `Bearer <CRON_SECRET>`

---

#### POST /api/v1/crypto/watch — Blockchain Watcher (Cron Worker)

Scans connected blockchain networks for incoming crypto transactions, checks confirmation counts, and marks crypto invoices as paid.

**Auth:** `Bearer <CRON_SECRET>`

---

### Checkout

---

#### POST /api/v1/checkout/sessions — Create Checkout Session

Create a checkout session for an existing payment. The session returns a short-lived token and URL for the customer to complete payment.

**Auth:** `payments:write`
**Idempotent:** Yes

##### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `payment_id` | string | Yes | Payment ID (`pay_*`) |

##### Request Example

```bash
curl -X POST "https://api.trustlypay.io/api/v1/checkout/sessions" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"payment_id": "pay_1704067200_a1b2c3d4"}'
```

##### Response (201 Created)

```json
{
  "data": {
    "id": "cs_1704067200_a1b2c3d4",
    "object": "checkout_session",
    "payment_id": "pay_1704067200_a1b2c3d4",
    "token": "ct_a1b2c3d4e5f6g7h8",
    "expires_at": "2026-09-05T13:30:00Z",
    "status": "open",
    "checkout_url": "https://checkout.trustlypay.io/pay/ct_a1b2c3d4e5f6g7h8",
    "created_at": "2026-09-05T13:00:00Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 400 | `INVALID_REQUEST` | Payment ID missing or invalid |
| 400 | `PAYMENT_EXPIRED` | Payment has expired |
| 400 | `CHECKOUT_COMPLETED` | Payment already completed |

---

#### GET /api/v1/checkout/sessions — List Checkout Sessions

List all checkout sessions for the merchant.

**Auth:** `payments:read`

##### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | `20` | Results per page (1–100) |
| `starting_after` | string | — | Cursor for pagination |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/checkout/sessions?limit=10"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "cs_1704067200_a1b2c3d4",
      "object": "checkout_session",
      "payment_id": "pay_1704067200_a1b2c3d4",
      "token": "ct_a1b2c3d4e5f6g7h8",
      "status": "open",
      "checkout_url": "https://checkout.trustlypay.io/pay/ct_a1b2c3d4e5f6g7h8",
      "created_at": "2026-09-05T13:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

---

#### GET /api/v1/checkout/{token} — Get Checkout Session (Public)

Retrieve checkout session details by token. This endpoint requires **no authentication** and is used by the customer-facing checkout page.

**Auth:** None

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `token` | string | Checkout token (`ct_*`) |

##### Request Example

```bash
curl "https://api.trustlypay.io/api/v1/checkout/ct_a1b2c3d4e5f6g7h8"
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "cs_1704067200_a1b2c3d4",
    "object": "checkout_session",
    "payment_id": "pay_1704067200_a1b2c3d4",
    "token": "ct_a1b2c3d4e5f6g7h8",
    "expires_at": "2026-09-05T13:30:00Z",
    "status": "open",
    "checkout_url": "https://checkout.trustlypay.io/pay/ct_a1b2c3d4e5f6g7h8",
    "created_at": "2026-09-05T13:00:00Z",
    "amount": 150000,
    "currency": "RUB",
    "description": "Order #12345",
    "order_id": "12345",
    "payment_method": "card",
    "payment_status": "created"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 400 | `CHECKOUT_COMPLETED` | Session already completed |
| 400 | `CHECKOUT_EXPIRED` | Session has expired |
| 404 | `NOT_FOUND` | Token not found |

---

### Customers

---

#### POST /api/v1/customers — Create Customer

Create a customer record for storing payment information and tracking.

**Auth:** `customers:write`
**Idempotent:** Yes

##### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `external_id` | string | No | Merchant's internal customer ID |
| `email` | string | No | Customer email |
| `phone` | string | No | Customer phone |
| `name` | string | No | Customer name |
| `metadata` | object | No | Key-value pairs |

##### Request Example

```bash
curl -X POST "https://api.trustlypay.io/api/v1/customers" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "user_9876",
    "email": "john@example.com",
    "name": "John Doe",
    "metadata": {
      "source": "registration"
    }
  }'
```

##### Response (201 Created)

```json
{
  "data": {
    "id": "cust_1704067200_a1b2c3d4",
    "object": "customer",
    "merchant_id": "merch_xxx",
    "external_id": "user_9876",
    "email": "john@example.com",
    "phone": null,
    "name": "John Doe",
    "metadata": {
      "source": "registration"
    },
    "status": "active",
    "created_at": "2026-09-05T13:00:00Z",
    "updated_at": "2026-09-05T13:00:00Z"
  }
}
```

---

#### GET /api/v1/customers — List Customers

**Auth:** `customers:read`

##### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | `20` | Results per page (1–100) |
| `starting_after` | string | — | Cursor for pagination |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/customers?limit=10"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "cust_1704067200_a1b2c3d4",
      "object": "customer",
      "external_id": "user_9876",
      "email": "john@example.com",
      "name": "John Doe",
      "status": "active",
      "created_at": "2026-09-05T13:00:00Z"
    }
  ],
  "has_more": true,
  "next_cursor": "cust_1704067200_a1b2c3d4"
}
```

---

#### GET /api/v1/customers/{id} — Get Customer

**Auth:** Any valid API key

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Customer ID (`cust_*`) |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/customers/cust_1704067200_a1b2c3d4"
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "cust_1704067200_a1b2c3d4",
    "object": "customer",
    "merchant_id": "merch_xxx",
    "external_id": "user_9876",
    "email": "john@example.com",
    "phone": null,
    "name": "John Doe",
    "metadata": {
      "source": "registration"
    },
    "status": "active",
    "created_at": "2026-09-05T13:00:00Z",
    "updated_at": "2026-09-05T13:00:00Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 404 | `CUSTOMER_NOT_FOUND` | Customer does not exist |

---

#### PATCH /api/v1/customers/{id} — Update Customer

**Auth:** `customers:write`

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Customer ID (`cust_*`) |

##### Request Body (all optional)

| Field | Type | Description |
|-------|------|-------------|
| `external_id` | string | Update external ID |
| `email` | string | Update email |
| `phone` | string | Update phone |
| `name` | string | Update name |
| `metadata` | object | Replace metadata entirely |

##### Request Example

```bash
curl -X PATCH "https://api.trustlypay.io/api/v1/customers/cust_1704067200_a1b2c3d4" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"name": "John A. Doe", "email": "john.doe@example.com"}'
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "cust_1704067200_a1b2c3d4",
    "object": "customer",
    "merchant_id": "merch_xxx",
    "external_id": "user_9876",
    "email": "john.doe@example.com",
    "phone": null,
    "name": "John A. Doe",
    "metadata": {
      "source": "registration"
    },
    "status": "active",
    "created_at": "2026-09-05T13:00:00Z",
    "updated_at": "2026-09-05T14:00:00Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 404 | `CUSTOMER_NOT_FOUND` | Customer does not exist |

---

#### DELETE /api/v1/customers/{id} — Delete Customer

Soft-deletes a customer. The customer record is retained with `status: "deleted"`.

**Auth:** `customers:write`

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Customer ID (`cust_*`) |

##### Request Example

```bash
curl -X DELETE "https://api.trustlypay.io/api/v1/customers/cust_1704067200_a1b2c3d4" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx"
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "cust_1704067200_a1b2c3d4",
    "deleted": true
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 404 | `CUSTOMER_NOT_FOUND` | Customer does not exist |

---

#### GET /api/v1/customers/{id}/payments — List Customer Payments

**Auth:** Any valid API key

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Customer ID (`cust_*`) |

##### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | `20` | Results per page (1–100) |
| `starting_after` | string | — | Cursor for pagination |
| `status` | string | — | Filter by status |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/customers/cust_1704067200_a1b2c3d4/payments?limit=5"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "pay_1704067200_a1b2c3d4",
      "object": "payment",
      "amount": 150000,
      "currency": "RUB",
      "status": "succeeded",
      "payment_method": "card",
      "created_at": "2026-09-05T13:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

---

### Refunds

---

#### POST /api/v1/refunds — Create Refund

Refund a payment (full or partial). Only payments in `succeeded` or `partially_refunded` status are eligible.

**Auth:** `refunds:write`
**Idempotent:** Yes

##### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `payment_id` | string | Yes | Payment to refund (`pay_*`) |
| `amount` | integer | No | Amount in kopecks/cents. Omit or `null` for full refund |
| `reason` | string | No | Refund reason (max 500 chars) |

##### Request Example

```bash
curl -X POST "https://api.trustlypay.io/api/v1/refunds" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund_order12345_partial1" \
  -d '{
    "payment_id": "pay_1704067200_a1b2c3d4",
    "amount": 50000,
    "reason": "Partial return"
  }'
```

##### Response (201 Created)

```json
{
  "data": {
    "id": "ref_1704067200_a1b2c3d4",
    "object": "refund",
    "payment_id": "pay_1704067200_a1b2c3d4",
    "amount": 50000,
    "currency": "RUB",
    "reason": "Partial return",
    "status": "pending",
    "provider_refund_id": null,
    "created_at": "2026-09-05T13:05:00Z",
    "updated_at": "2026-09-05T13:05:00Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 400 | `INVALID_REQUEST` | Payment in wrong status |
| 400 | `INVALID_REQUEST` | Refund amount exceeds refundable amount |
| 400 | `INVALID_REQUEST` | Payment already fully refunded |
| 404 | `PAYMENT_NOT_FOUND` | Payment does not exist |

##### Webhook Effects

Fires `refund.created`, then `refund.succeeded` or `refund.failed`. The payment's `payment.refunded` or `payment.partially_refunded` event also fires.

---

#### GET /api/v1/refunds — List Refunds

**Auth:** `refunds:read`

##### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `payment_id` | string | — | Filter by payment ID |
| `status` | string | — | Filter by status |
| `limit` | integer | `20` | Results per page (1–100) |
| `starting_after` | string | — | Cursor for pagination |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/refunds?status=succeeded&limit=10"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "ref_1704067200_a1b2c3d4",
      "object": "refund",
      "payment_id": "pay_1704067200_a1b2c3d4",
      "amount": 50000,
      "currency": "RUB",
      "status": "succeeded",
      "created_at": "2026-09-05T13:05:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

---

#### GET /api/v1/refunds/{id} — Get Refund

**Auth:** Any valid API key

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Refund ID (`ref_*`) |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/refunds/ref_1704067200_a1b2c3d4"
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "ref_1704067200_a1b2c3d4",
    "object": "refund",
    "payment_id": "pay_1704067200_a1b2c3d4",
    "amount": 50000,
    "currency": "RUB",
    "reason": "Partial return",
    "status": "succeeded",
    "provider_refund_id": "re_xxx",
    "created_at": "2026-09-05T13:05:00Z",
    "updated_at": "2026-09-05T13:06:10Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 404 | `REFUND_NOT_FOUND` | Refund does not exist |

---

### Payouts

---

#### POST /api/v1/payouts — Create Payout

Send funds to an external recipient (bank account, wallet, etc.).

**Auth:** `payouts:write`
**Idempotent:** Yes

##### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `amount` | integer | Yes | Amount in kopecks/cents |
| `currency` | string | No | ISO 4217 (default: `"RUB"`) |
| `recipient` | string | Yes | Recipient identifier (card number, wallet, bank account) |
| `description` | string | No | Payout description |

##### Request Example

```bash
curl -X POST "https://api.trustlypay.io/api/v1/payouts" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 100000,
    "currency": "RUB",
    "recipient": "2202201234567890",
    "description": "Payout for services"
  }'
```

##### Response (201 Created)

```json
{
  "data": {
    "id": "pay_1704067200_b3c4d5e6",
    "object": "payout",
    "amount": 100000,
    "currency": "RUB",
    "status": "pending",
    "recipient": "2202201234567890",
    "description": "Payout for services",
    "created_at": "2026-09-05T13:00:00Z",
    "updated_at": "2026-09-05T13:00:00Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 400 | `INVALID_REQUEST` | Missing required fields |
| 400 | `AMOUNT_TOO_SMALL` | Amount < 1 |
| 400 | `AMOUNT_TOO_LARGE` | Amount > 99,999,999 |
| 409 | `IDEMPOTENCY_CONFLICT` | Same key, different body |

##### Webhook Effects

Fires `payout.created`, then `payout.succeeded` or `payout.failed`.

---

#### GET /api/v1/payouts — List Payouts

**Auth:** `payouts:read`

##### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | `20` | Results per page (1–100) |
| `starting_after` | string | — | Cursor for pagination |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/payouts?limit=10"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "pay_1704067200_b3c4d5e6",
      "object": "payout",
      "amount": 100000,
      "currency": "RUB",
      "status": "succeeded",
      "recipient": "2202201234567890",
      "created_at": "2026-09-05T13:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

---

### Balance

---

#### GET /api/v1/balance — Get Balance

Returns current available, processing, and reserved balances.

**Auth:** `balance:read`

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/balance"
```

##### Response (200 OK)

```json
{
  "data": {
    "available": 5000000,
    "processing": 150000,
    "reserved": 300000,
    "currency": "RUB"
  }
}
```

---

#### GET /api/v1/balance/transactions — List Balance Transactions

**Auth:** `balance:read`

##### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `type` | string | — | Filter: `payment`, `payout`, `refund`, `fee` |
| `from` | string | — | ISO 8601 start date |
| `to` | string | — | ISO 8601 end date |
| `limit` | integer | `20` | Results per page (1–100) |
| `starting_after` | string | — | Cursor for pagination |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/balance/transactions?type=payment&from=2026-09-01T00:00:00Z&to=2026-09-05T23:59:59Z"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "btx_a1b2c3d4",
      "type": "payment",
      "amount": 150000,
      "balance_after": 5150000,
      "description": "Payment pay_1704067200_a1b2c3d4",
      "created_at": "2026-09-05T13:01:22Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

---

### Webhooks

---

#### POST /api/v1/webhooks/endpoints — Create Webhook Endpoint

Register a URL to receive webhook events. The `secret` is returned **only on creation** and must be stored.

**Auth:** `webhooks:write`
**Idempotent:** Yes

##### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | string | Yes | HTTPS URL (SSRF-protected) |
| `events` | string[] | Yes | Non-empty list of event types to subscribe to |

##### Valid Event Types

| Event |
|-------|
| `payment.created` |
| `payment.pending` |
| `payment.succeeded` |
| `payment.failed` |
| `payment.canceled` |
| `payment.expired` |
| `payment.refunded` |
| `refund.created` |
| `refund.succeeded` |
| `refund.failed` |
| `payout.created` |
| `payout.succeeded` |
| `payout.failed` |
| `subscription.created` |
| `subscription.updated` |
| `subscription.canceled` |

##### Request Example

```bash
curl -X POST "https://api.trustlypay.io/api/v1/webhooks/endpoints" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/trustlypay",
    "events": ["payment.succeeded", "payment.failed", "refund.succeeded"]
  }'
```

##### Response (201 Created)

```json
{
  "data": {
    "id": "whend_1704067200_a1b2c3d4",
    "object": "webhook_endpoint",
    "project_id": "prj_001",
    "url": "https://example.com/webhooks/trustlypay",
    "events": ["payment.succeeded", "payment.failed", "refund.succeeded"],
    "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "is_active": true,
    "created_at": "2026-09-05T13:00:00Z",
    "updated_at": "2026-09-05T13:00:00Z"
  }
}
```

**Store the `secret` securely. It will not be shown again.**

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 400 | `INVALID_URL` | Malformed URL |
| 400 | `SSRF_BLOCKED` | Private/local URL blocked |
| 400 | `INVALID_REQUEST` | Empty events array |

---

#### GET /api/v1/webhooks/endpoints — List Webhook Endpoints

**Auth:** `webhooks:read`

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/webhooks/endpoints"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "whend_1704067200_a1b2c3d4",
      "object": "webhook_endpoint",
      "project_id": "prj_001",
      "url": "https://example.com/webhooks/trustlypay",
      "events": ["payment.succeeded", "payment.failed"],
      "secret": "whsec_****",
      "is_active": true,
      "created_at": "2026-09-05T13:00:00Z",
      "updated_at": "2026-09-05T13:00:00Z"
    }
  ],
  "has_more": false
}
```

Note: The `secret` is masked as `whsec_****` in all responses except creation.

---

#### GET /api/v1/webhooks/endpoints/{id} — Get Webhook Endpoint

**Auth:** Any valid API key

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Endpoint ID (`whend_*`) |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/webhooks/endpoints/whend_1704067200_a1b2c3d4"
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "whend_1704067200_a1b2c3d4",
    "object": "webhook_endpoint",
    "project_id": "prj_001",
    "url": "https://example.com/webhooks/trustlypay",
    "events": ["payment.succeeded", "payment.failed", "refund.succeeded"],
    "secret": "whsec_****",
    "is_active": true,
    "created_at": "2026-09-05T13:00:00Z",
    "updated_at": "2026-09-05T13:00:00Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 404 | `WEBHOOK_ENDPOINT_NOT_FOUND` | Endpoint does not exist |

---

#### PATCH /api/v1/webhooks/endpoints/{id} — Update Webhook Endpoint

**Auth:** `webhooks:write`

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Endpoint ID (`whend_*`) |

##### Request Body (all optional)

| Field | Type | Description |
|-------|------|-------------|
| `url` | string | New HTTPS URL |
| `events` | string[] | New event list |
| `is_active` | boolean | Enable/disable endpoint |

##### Request Example

```bash
curl -X PATCH "https://api.trustlypay.io/api/v1/webhooks/endpoints/whend_1704067200_a1b2c3d4" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"events": ["payment.succeeded", "payment.failed", "refund.succeeded", "payout.succeeded"]}'
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "whend_1704067200_a1b2c3d4",
    "object": "webhook_endpoint",
    "project_id": "prj_001",
    "url": "https://example.com/webhooks/trustlypay",
    "events": ["payment.succeeded", "payment.failed", "refund.succeeded", "payout.succeeded"],
    "secret": "whsec_****",
    "is_active": true,
    "created_at": "2026-09-05T13:00:00Z",
    "updated_at": "2026-09-05T14:00:00Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 404 | `WEBHOOK_ENDPOINT_NOT_FOUND` | Endpoint does not exist |

---

#### DELETE /api/v1/webhooks/endpoints/{id} — Delete Webhook Endpoint

**Auth:** `webhooks:write`

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Endpoint ID (`whend_*`) |

##### Request Example

```bash
curl -X DELETE "https://api.trustlypay.io/api/v1/webhooks/endpoints/whend_1704067200_a1b2c3d4" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx"
```

##### Response (200 OK)

```json
{
  "data": {
    "id": "whend_1704067200_a1b2c3d4",
    "deleted": true
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 404 | `WEBHOOK_ENDPOINT_NOT_FOUND` | Endpoint does not exist |

---

#### GET /api/v1/webhooks/endpoints/{id}/deliveries — List Webhook Deliveries

**Auth:** Any valid API key

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Endpoint ID (`whend_*`) |

##### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | `20` | Results per page (1–100) |

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/webhooks/endpoints/whend_1704067200_a1b2c3d4/deliveries?limit=5"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "whdel_1704067200_a1b2c3d4",
      "object": "webhook_delivery",
      "endpoint_id": "whend_1704067200_a1b2c3d4",
      "event_id": "evt_a1b2c3d4",
      "status": "succeeded",
      "attempt": 1,
      "max_attempts": 6,
      "next_retry_at": null,
      "response_status": 200,
      "response_body": "{\"ok\":true}",
      "created_at": "2026-09-05T13:01:25Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

---

#### POST /api/v1/webhooks/endpoints/{id}/deliveries/{delivery_id}/retry — Retry Webhook Delivery

Manually retry a failed webhook delivery.

**Auth:** `webhooks:write`

##### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Endpoint ID (`whend_*`) |
| `delivery_id` | string | Delivery ID (`whdel_*`) |

##### Request Example

```bash
curl -X POST "https://api.trustlypay.io/api/v1/webhooks/endpoints/whend_1704067200_a1b2c3d4/deliveries/whdel_1704067200_a1b2c3d4/retry" \
  -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{}'
```

##### Response (201 Created)

```json
{
  "data": {
    "id": "whdel_1704067200_b5e6f7g8",
    "object": "webhook_delivery",
    "endpoint_id": "whend_1704067200_a1b2c3d4",
    "event_id": "evt_a1b2c3d4",
    "status": "pending",
    "attempt": 4,
    "max_attempts": 6,
    "next_retry_at": null,
    "response_status": null,
    "response_body": null,
    "created_at": "2026-09-05T14:00:00Z"
  }
}
```

##### Errors

| HTTP | Code | Condition |
|------|------|-----------|
| 404 | `WEBHOOK_ENDPOINT_NOT_FOUND` | Endpoint does not exist |

---

### Projects

---

#### GET /api/v1/projects — List Projects

**Auth:** `payments:read`

##### Request Example

```bash
curl -H "X-API-Key: op_test_xxxxxxxxxxxxxxxx" \
  "https://api.trustlypay.io/api/v1/projects"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "prj_001",
      "object": "project",
      "merchant_id": "merch_xxx",
      "name": "Main Store",
      "description": "Primary payment integration",
      "status": "active",
      "environment": "test",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  ],
  "has_more": false
}
```

---

### Payment Methods

---

#### GET /api/v1/payment-methods — List Payment Methods

Returns available payment methods and their limits. No authentication required.

**Auth:** None

##### Request Example

```bash
curl "https://api.trustlypay.io/api/v1/payment-methods"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "id": "pm_card",
      "method": "card",
      "name": "Bank Card (Visa, Mastercard, МИР)",
      "enabled": true,
      "min_amount": 100,
      "max_amount": 99999999
    },
    {
      "id": "pm_sbp",
      "method": "sbp",
      "name": "Система Быстрых Платежей",
      "enabled": true,
      "min_amount": 100,
      "max_amount": 99999999
    },
    {
      "id": "pm_link",
      "method": "link",
      "name": "Payment Link",
      "enabled": true,
      "min_amount": 100,
      "max_amount": 99999999
    },
    {
      "id": "pm_crypto",
      "method": "crypto",
      "name": "Cryptocurrency",
      "enabled": true,
      "min_amount": 500,
      "max_amount": 50000000
    }
  ]
}
```

---

### Limits

---

#### GET /api/v1/limits — Payment Limits

Returns payment limits by currency and method. No authentication required.

**Auth:** None

##### Request Example

```bash
curl "https://api.trustlypay.io/api/v1/limits"
```

##### Response (200 OK)

```json
{
  "data": [
    {
      "min_amount": 100,
      "max_amount": 99999999,
      "currency": "RUB",
      "payment_method": "card",
      "daily_limit": 50000000,
      "monthly_limit": 500000000
    },
    {
      "min_amount": 100,
      "max_amount": 99999999,
      "currency": "RUB",
      "payment_method": "sbp",
      "daily_limit": 50000000,
      "monthly_limit": 500000000
    }
  ]
}
```

---

### Health

---

#### GET /api/v1/health — Health Check

Returns system health status. No authentication required.

**Auth:** None

##### Request Example

```bash
curl "https://api.trustlypay.io/api/v1/health"
```

##### Response (200 OK)

```json
{
  "data": {
    "status": "healthy",
    "version": "1.2.3",
    "timestamp": "2026-09-05T13:00:00Z",
    "checks": {
      "api": "ok",
      "provider": "ok",
      "database": "ok"
    }
  }
}
```

---

### OpenAPI

---

#### GET /api/v1/openapi — OpenAPI Specification

Returns the OpenAPI 3.1 specification in JSON format. No authentication required.

**Auth:** None

##### Request Example

```bash
curl "https://api.trustlypay.io/api/v1/openapi"
```

##### Response (200 OK)

Returns the complete OpenAPI 3.1 JSON specification.

---

## Webhooks

### Overview

Webhooks are HTTP POST requests sent to your registered endpoint URLs when events occur in your Trustly Pay account. All webhook endpoints are managed via the [Webhooks API](#webhooks-1).

### Event Types

| Event | Trigger |
|-------|---------|
| `payment.created` | Payment is created |
| `payment.pending` | Payment is processing with provider |
| `payment.succeeded` | Payment completed successfully |
| `payment.failed` | Payment failed |
| `payment.canceled` | Payment was canceled |
| `payment.expired` | Payment expired |
| `payment.refunded` | Payment was fully refunded |
| `refund.created` | Refund initiated |
| `refund.succeeded` | Refund completed |
| `refund.failed` | Refund failed |
| `payout.created` | Payout initiated |
| `payout.succeeded` | Payout completed |
| `payout.failed` | Payout failed |
| `subscription.created` | Subscription created |
| `subscription.updated` | Subscription updated |
| `subscription.canceled` | Subscription canceled |

### Webhook Payload

```json
{
  "id": "evt_a1b2c3d4",
  "object": "event",
  "type": "payment.succeeded",
  "created_at": "2026-09-05T13:01:22Z",
  "data": {
    "id": "pay_1704067200_a1b2c3d4",
    "object": "payment",
    "amount": 150000,
    "currency": "RUB",
    "status": "succeeded"
  }
}
```

### Signature Verification

Each webhook request includes an `X-TrustlyPay-Signature` header containing an HMAC-SHA256 signature of the raw request body using your endpoint secret.

```
X-TrustlyPay-Signature: hmac_sha256(payload, endpoint_secret)
```

**Verify using timing-safe comparison** to prevent timing attacks:

```javascript
import crypto from 'crypto';

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
```

```python
import hmac, hashlib

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)
```

### Retry Policy

Failed deliveries are retried with exponential backoff:

| Attempt | Delay |
|---------|-------|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 6 hours |
| 7 | 24 hours |

Maximum **6 attempts** total (7 including initial). You can also manually retry via the [Retry Delivery](#post-webhooksendpointsendpointiddeliveriesdelivery_idretry) endpoint.

### Delivery Status

Each delivery is tracked with:

| Field | Description |
|-------|-------------|
| `status` | `pending`, `succeeded`, or `failed` |
| `attempt` | Current attempt number (1–6) |
| `response_status` | HTTP status code received |
| `response_body` | Response body (truncated to 1KB) |
| `next_retry_at` | Next scheduled retry time |

### Idempotency

Webhook event IDs are unique. Your endpoint should handle potential duplicate deliveries by checking the event ID and maintaining a set of processed IDs.

---

## Merchant Portal API

The Trustly Pay merchant dashboard uses cookie/session authenticated routes under `/api/merchant/...` and `/api/user`:

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/merchant` | `GET`, `PATCH` | Get or update merchant profile, fee payer, requisites (payout card/crypto) |
| `/api/merchant/dashboard` | `GET` | Dashboard statistics, turnover, conversion, payment volume by period (`today`, `week`, `month`, `all`) |
| `/api/merchant/balance` | `GET` | Real-time available, processing, reserved balances, freeze status and ledger entries |
| `/api/merchant/payouts` | `GET`, `POST` | List payouts or request a withdrawal (minimum 1000 ₽ or custom, card Luhn validated) |
| `/api/merchant/payment-links` | `GET`, `POST` | List or create reusable 30-minute payment links for projects |
| `/api/merchant/payment-links/{id}` | `GET`, `PATCH`, `DELETE` | Manage a specific payment link |
| `/api/merchant/payment-methods` | `GET` | List merchant's enabled and available payment methods |
| `/api/merchant/projects` | `GET`, `POST`, `PATCH` | List, create, or update project websites and success/fail URLs |
| `/api/merchant/keys` | `GET`, `POST`, `DELETE` | Manage Live and Test API keys |
| `/api/merchant/notifications` | `GET`, `PATCH` | System notifications and broadcast updates |
| `/api/merchant/webhooks` | `GET`, `POST` | Configure merchant webhook destination and view delivery history |
| `/api/public/brand` | `GET` | Public branding info, manager Telegram contacts for registered and guest users |

---

## Error Reference

### Complete Error Table

| HTTP | Code | Meaning |
|------|------|---------|
| 400 | `INVALID_REQUEST` | Invalid parameters |
| 400 | `PAYMENT_EXPIRED` | Payment has expired |
| 400 | `PAYMENT_CANCELED` | Payment already canceled |
| 400 | `PAYMENT_METHOD_DISABLED` | Method disabled for merchant |
| 400 | `PAYMENT_LIMIT_EXCEEDED` | Amount outside limits |
| 400 | `AMOUNT_TOO_SMALL` | Amount < 1 |
| 400 | `AMOUNT_TOO_LARGE` | Amount > 99,999,999 |
| 400 | `INVALID_CURRENCY` | Bad currency format |
| 400 | `INVALID_URL` | Bad URL format |
| 400 | `SSRF_BLOCKED` | Private/local URL blocked |
| 400 | `CHECKOUT_EXPIRED` | Session expired |
| 400 | `CHECKOUT_COMPLETED` | Session already completed |
| 400 | `FEATURE_NOT_SUPPORTED` | Feature not available |
| 401 | `UNAUTHORIZED` | Missing/invalid API key |
| 401 | `INVALID_CREDENTIALS` | Bad login credentials |
| 401 | `API_KEY_REVOKED` | Key has been revoked |
| 401 | `INVALID_API_KEY` | Key not found |
| 403 | `FORBIDDEN` | Insufficient permissions |
| 403 | `MERCHANT_SUSPENDED` | Merchant suspended/blocked |
| 403 | `PAYMENTS_SUSPENDED` | Payments disabled |
| 403 | `INVALID_SCOPE` | Missing required scope |
| 404 | `NOT_FOUND` | Resource not found |
| 404 | `PAYMENT_NOT_FOUND` | Payment not found |
| 404 | `CUSTOMER_NOT_FOUND` | Customer not found |
| 404 | `REFUND_NOT_FOUND` | Refund not found |
| 404 | `WEBHOOK_ENDPOINT_NOT_FOUND` | Endpoint not found |
| 404 | `API_KEY_NOT_FOUND` | API key not found |
| 404 | `PROJECT_NOT_FOUND` | Project not found |
| 409 | `CONFLICT` | Resource conflict |
| 409 | `PAYMENT_ALREADY_PAID` | Payment already paid |
| 409 | `IDEMPOTENCY_CONFLICT` | Same key, different body |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `WEBHOOK_ERROR` | Webhook delivery error |
| 502 | `PROVIDER_ERROR` | Provider returned error |
| 503 | `PROVIDER_UNAVAILABLE` | Provider unavailable |
| 504 | `PROVIDER_TIMEOUT` | Provider timeout |

---

## Security

### API Key Security

- Keys are prefixed: `op_test_*` for sandbox, `op_live_*` for production
- Webhook secrets are prefixed: `whsec_*`
- Store keys securely; never expose in client-side code
- Rotate keys immediately if compromised
- Use test keys during development

### Transport Security

- All API endpoints require HTTPS
- TLS 1.2+ only

### Webhook URL Security

- Webhook URLs must use HTTPS
- SSRF protection blocks private IPs, localhost, and internal networks
- Verify webhook signatures on your endpoint

### Best Practices

- Never hardcode API keys in source code
- Use environment variables for key storage
- Implement proper logging (redact keys)
- Restrict API key scopes to minimum required permissions
- Monitor webhook delivery logs for anomalies

---

## SDKs

### JavaScript / TypeScript

```bash
npm install @trustlypay/sdk
```

```typescript
import { TrustlyPay } from '@trustlypay/sdk';

const client = new TrustlyPay({
  apiKey: process.env.TRUSTLYPAY_API_KEY,
});

const payment = await client.payments.create({
  amount: 150000,
  currency: 'RUB',
  description: 'Order #12345',
});
```

### Python

```bash
pip install trustlypay
```

```python
from trustlypay import TrustlyPay

client = TrustlyPay(api_key="op_test_xxxx")

payment = client.payments.create(
    amount=150000,
    currency="RUB",
    description="Order #12345",
)
```

### PHP

```bash
composer require trustlypay/php-sdk
```

```php
use TrustlyPay\TrustlyPay;

$client = new TrustlyPay('op_test_xxxx');

$payment = $client->payments->create([
    'amount' => 150000,
    'currency' => 'RUB',
    'description' => 'Order #12345',
]);
```

---

## Telegram Integration

Trustly Pay integrates with Telegram bots for seamless in-chat payments.

### Flow

1. Bot sends payment link to user
2. User completes payment via TrustlyPay checkout
3. TrustlyPay fires `payment.succeeded` webhook
4. Bot receives webhook and confirms delivery/service

### Bot Command Example

```
/user
你好！点击下方按钮完成支付：

[Pay 150 RUB] → https://checkout.trustlypay.io/pay/ct_xxx

支付完成后我们会自动确认。
```

### Webhook Handler

```python
@app.post("/webhooks/trustlypay")
async def handle_webhook(request: Request):
    payload = await request.body()
    signature = request.headers.get("X-TrustlyPay-Signature")

    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        raise HTTPException(status_code=400)

    event = json.loads(payload)

    if event["type"] == "payment.succeeded":
        payment = event["data"]
        await notify_telegram_user(
            user_id=payment["metadata"]["telegram_user_id"],
            message=f"Payment {payment['id']} confirmed!"
        )
```

---

## Website Integration

### Client-Server Payment Flow

1. **Create payment** on your server
2. **Redirect customer** to `checkout_url`
3. **Customer completes payment**
4. **TrustlyPay redirects** to `return_url`
5. **Webhook confirms** payment on your server

### Minimal Integration (Node.js)

```javascript
// 1. Create payment
const response = await fetch('https://api.trustlypay.io/api/v1/payments', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.TRUSTLYPAY_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': `order_${orderId}_${Date.now()}`,
  },
  body: JSON.stringify({
    amount: 150000,
    currency: 'RUB',
    description: `Order #${orderId}`,
    order_id: orderId,
    return_url: `https://example.com/order/${orderId}/success`,
    cancel_url: `https://example.com/order/${orderId}/cancel`,
  }),
});

const { data: payment } = await response.json();

// 2. Redirect to checkout
window.location.href = payment.checkout_url;

// 3. Handle webhook (on your server)
app.post('/webhooks/trustlypay', async (req, res) => {
  const event = req.body;

  if (event.type === 'payment.succeeded') {
    const paymentId = event.data.id;
    await fulfillOrder(paymentId);
  }

  res.json({ ok: true });
});
```

### Checkout Page (HTML)

```html
<form action="https://api.trustlypay.io/api/v1/payments" method="POST">
  <input type="hidden" name="amount" value="150000">
  <input type="hidden" name="currency" value="RUB">
  <button type="submit">Pay Now</button>
</form>
```

---

## AI Integration

### OpenAPI Spec

The complete API is described in OpenAPI 3.1 format:

```
GET /api/v1/openapi
```

Use this spec to generate API clients, documentation, or integrate with AI tools that support OpenAPI.

### AI Context

When building AI agents or assistants, you can provide the OpenAPI spec as context. The spec includes all endpoint descriptions, parameter schemas, and response types needed for an AI to make correct API calls.

### Example: AI Assistant Integration

```python
import json
import requests

# Load OpenAPI spec as AI context
openapi = requests.get(
    "https://api.trustlypay.io/api/v1/openapi"
).json()

# AI can now understand and generate API calls
context = f"""
You are a payment assistant. Use the TrustlyPay API to process payments.

API Specification:
{json.dumps(openapi, indent=2)}

When a user wants to pay:
1. Ask for amount and currency
2. Call POST /api/v1/payments
3. Return the checkout_url
"""
```

---

## Changelog

### v1.0.0 (2026-09-05)

- Initial release
- Payments (create, list, get, cancel)
- Checkout sessions (create, list, get)
- Customers (CRUD, payments)
- Refunds (create, list, get)
- Payouts (create, list)
- Balance (get, transactions)
- Webhooks (endpoints, deliveries, retry)
- Projects (list)
- Payment methods (list)
- Limits (get)
- Health check
- OpenAPI spec

---

*API Version: v1 | Base URL: `https://api.trustlypay.io`*
