> For the complete documentation index, see [llms.txt](https://paykilla.gitbook.io/paykilla-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://paykilla.gitbook.io/paykilla-docs/api-integration/integration.md).

# Integration

## Overview

PayKilla V2 API enables you to accept cryptocurrency payments. This guide describes how to integrate with the API to create invoices and process payments.

**Base URL:** `https://account-api.paykilla.com`

**Widget URL:** `https://gopay.paykilla.com/{invoice_id}`

***

## Quick Start

### 1. Obtain API Keys

In your PayKilla dashboard open **Settings → API keys**, click **Generate API keys** and select the permissions for the key. Available permissions: `READ`, `INVOICE`, `TRADE`, `WITHDRAWAL`. To work with invoices the key needs **`INVOICE`**.

You will receive:

* `publicKey` — public key (passed in the `X-API-KEY` header)
* `secretKey` — secret key (used for signing requests; keep it on your backend only)

### 2. Set Up Environment Variables

```env
PAYKILLA_V2_API_KEY=your_public_key
PAYKILLA_V2_SECRET_KEY=your_secret_key
```

### 3. Create an Invoice and Redirect the User

```typescript
// 1. On your server: create an invoice via API (the secret key never leaves the server)
const invoice = await createInvoice({
  type: "FIAT_BASED",
  purpose: "Payment for Order 123",
  currency: "USD",
  totalPrice: "100",
  paymentCurrencies: ["BTC", "ETH", "USDTTRC"],
})

// 2. Send this URL to the browser and redirect the user to the payment page
const paymentUrl = `https://gopay.paykilla.com/${invoice.id}`
```

***

## Authentication

### Signature Mechanism

PayKilla V2 uses **HMAC-SHA256** for request authentication.

#### Parameters

| Parameter    | Description                                                   |
| ------------ | ------------------------------------------------------------- |
| `timestamp`  | Current Unix timestamp in milliseconds (`Date.now()`)         |
| `recvWindow` | Request validity window in milliseconds (recommended: `5000`) |
| `signature`  | HMAC-SHA256 signature of the request                          |

The `recvWindow` parameter defines how long the request is valid. If the server receives a request with a timestamp older than `recvWindow` milliseconds, it will be rejected. This prevents replay attacks.

#### For GET Requests:

1. Collect your own query parameters (if any), then `timestamp` and `recvWindow`
2. Join them as `key=value&key=value`, in the order in which they are sent, using the **raw values, without URL encoding** (`qs.stringify(params, { encode: false })`)
3. Sign that string with HMAC-SHA256
4. Send the same parameters in the query string, URL-encoded as usual, and append `signature`

#### For POST Requests:

1. Sign only `timestamp` + `recvWindow` (body is NOT signed)
2. Pass `timestamp`, `recvWindow`, `signature` in the query string
3. Pass data in the body as JSON

**Why the difference?** POST body contains the actual data and is sent separately. The signature authenticates the request timing, while the body integrity is ensured by HTTPS.

### Implementation Example

```typescript
import { createHmac } from "crypto"
import axios from "axios"
import qs from "qs"

const client = axios.create({
  baseURL: "https://account-api.paykilla.com",
  headers: {
    "X-API-KEY": process.env.PAYKILLA_V2_API_KEY,
  },
})

function sign(data: string, secretKey: string): string {
  return createHmac("sha256", secretKey)
    .update(data)
    .digest("hex")
}

// GET request
async function getCurrencies() {
  const timestamp = Date.now()
  const recvWindow = 5000

  const dataToSign = qs.stringify({ timestamp, recvWindow })
  const signature = sign(dataToSign, process.env.PAYKILLA_V2_SECRET_KEY!)

  const response = await client.get("/api/v2/currency", {
    params: { timestamp, recvWindow, signature }
  })

  return response.data
}

// POST request
async function createInvoice(invoiceData: object) {
  const timestamp = Date.now()
  const recvWindow = 5000

  // For POST, sign only timestamp + recvWindow
  const dataToSign = qs.stringify({ timestamp, recvWindow })
  const signature = sign(dataToSign, process.env.PAYKILLA_V2_SECRET_KEY!)

  const response = await client.post("/api/v2/invoice", invoiceData, {
    params: { timestamp, recvWindow, signature }
  })

  return response.data
}
```

### Requests with Query Parameters

For GET requests your own parameters (paging, filters) are part of the signed string. The server calculates the signature over the **decoded** parameter values, so sign the raw values and let the query string itself be URL-encoded as usual:

```typescript
async function signedGet(path: string, params: Record<string, string | number | boolean> = {}) {
  const all = { ...params, timestamp: Date.now(), recvWindow: 5000 }
  // The signature is calculated over the raw (not URL-encoded) values
  const signature = sign(qs.stringify(all, { encode: false }), process.env.PAYKILLA_V2_SECRET_KEY!)
  // The query string itself is URL-encoded as usual
  const response = await client.get(`${path}?${qs.stringify(all)}&signature=${signature}`)
  return response.data
}

// Usage
const page = await signedGet("/api/v2/invoice", {
  page: 1,
  pageSize: 20,
  status: "SUCCESSFUL",
  dateFrom: "2026-01-01T00:00:00.000Z",
})
```

| Value                      | String to sign                      | Sent in the URL                         |
| -------------------------- | ----------------------------------- | --------------------------------------- |
| `USDTTRC`                  | `currency=USDTTRC`                  | `currency=USDTTRC`                      |
| `2026-01-01T00:00:00.000Z` | `dateFrom=2026-01-01T00:00:00.000Z` | `dateFrom=2026-01-01T00%3A00%3A00.000Z` |

If you sign the URL-encoded form (`%3A`, `%20`), the API answers `400 Invalid signature`. For values that consist only of letters, digits, `-`, `_` and `.` both forms are identical, which is why the shorter samples on this page work as they are.

### Response Format

Responses are plain JSON without an envelope: an endpoint that returns one object returns the object itself, and list endpoints return either an array or an object with `metadata` and `data` (see the description of each endpoint).

Not every request returns a body. Creating an invoice returns the invoice, but creating a withdrawal or an exchange and updating a static address return an **empty body**; their descriptions say how to get the ID of what was created. Read only the fields that the description of a request lists; if it lists none, do not rely on the response body.

Errors have the following body:

```json
{
  "message": "Invalid signature",
  "error": "Bad Request",
  "statusCode": 400
}
```

For validation errors `message` is an array of strings. `500` responses contain only `message` and `statusCode`. Authentication errors:

| Situation                              | HTTP status | `message`                      |
| -------------------------------------- | ----------- | ------------------------------ |
| `X-API-KEY` header is missing          | 403         | `Forbidden resource`           |
| `signature` parameter is missing       | 403         | `Forbidden resource`           |
| Signature does not match               | 400         | `Invalid signature`            |
| `timestamp` is older than `recvWindow` | 400         | `Timestamp outside recvWindow` |
| The path does not exist                | 404         | `Cannot GET /api/v2/...`       |

***

## Creating Invoices

### Endpoint

```
POST /api/v2/invoice
```

A successful request is answered with `201 Created` and the created invoice in the body.

### Request Parameters

| Parameter            | Type      | Required    | Description                                                                |
| -------------------- | --------- | ----------- | -------------------------------------------------------------------------- |
| `type`               | string    | Yes         | Invoice type: `FIAT_BASED`, `FIXED_AMOUNT`, or `OPEN_AMOUNT`               |
| `purpose`            | string    | Yes         | Payment purpose (displayed to customer)                                    |
| `currency`           | string    | Yes         | Currency (`USD`, `EUR`, `BTC`, etc.)                                       |
| `totalPrice`         | string    | Conditional | Payment amount (required unless `items` provided or type is `OPEN_AMOUNT`) |
| `paymentCurrencies`  | string\[] | Yes         | Cryptocurrencies accepted for payment                                      |
| `description`        | string    | No          | Internal payment description                                               |
| `expiredAt`          | string    | No          | ISO expiration date (default: no expiration)                               |
| `payerEmail`         | string    | No          | Customer's email address                                                   |
| `items`              | object\[] | No          | List of items (for detailed invoices)                                      |
| `clientOrderId`      | string    | No          | Your unique order identifier for correlation (1-255 chars)                 |
| `userPaysServiceFee` | boolean   | No          | Who pays service fee: `true` = payer, `false` = merchant (default: `true`) |
| `userPaysNetworkFee` | boolean   | No          | Who pays network fee: `true` = payer, `false` = merchant (default: `true`) |
| `urls`               | object\[] | No          | Redirect URLs configuration (see Redirect URLs section)                    |

### Client Order ID

The `clientOrderId` parameter allows you to pass your own order identifier for tracking and correlation.

**Restrictions:**

| Property           | Value                                          |
| ------------------ | ---------------------------------------------- |
| Length             | 1-255 characters                               |
| Allowed characters | `A-Z`, `a-z`, `0-9`, `-`, `_`, `.`             |
| Uniqueness         | Must be unique per merchant (if provided)      |
| If not provided    | In the response `clientOrderId` will be `null` |

**Example values:** `order-123`, `INV_2025_001`, `abc.def_123`

### Fee Configuration

By default, the payer (customer) pays all fees on top of the invoice amount. You can configure who pays each fee type using the following parameters:

| Parameter            | Default | Description                                  |
| -------------------- | ------- | -------------------------------------------- |
| `userPaysServiceFee` | `true`  | Service fee (percentage charged by PayKilla) |
| `userPaysNetworkFee` | `true`  | Network fee (blockchain transaction fee)     |

**How it works:**

| Configuration         | Payer sees                     | Merchant receives                       |
| --------------------- | ------------------------------ | --------------------------------------- |
| Both `true` (default) | Invoice amount + all fees      | Full invoice amount                     |
| Both `false`          | Invoice amount                 | Invoice amount minus fees               |
| Mixed                 | Invoice amount + selected fees | Invoice amount minus merchant-paid fees |

**Example: Merchant pays all fees**

```typescript
// Invoice for $100 where merchant absorbs all fees
const invoice = await createInvoice({
  type: "FIAT_BASED",
  purpose: "Order 12345",
  currency: "USD",
  totalPrice: "100",
  paymentCurrencies: ["USDTTRC"],
  userPaysServiceFee: false,  // Merchant pays service fee
  userPaysNetworkFee: false,  // Merchant pays network fee
})
// Customer pays 100 USD worth of USDT at the current rate
// Merchant receives 100 USDT minus the service fee and the network fee
```

**Example: Split fees**

```typescript
// Customer pays network fee, merchant pays service fee
const invoice = await createInvoice({
  type: "FIAT_BASED",
  purpose: "Order 12345",
  currency: "USD",
  totalPrice: "100",
  paymentCurrencies: ["USDTTRC"],
  userPaysServiceFee: false,  // Merchant pays service fee
  userPaysNetworkFee: true,   // Customer pays network fee
})
```

**Fee breakdown:**

The invoice response includes the `orderOperationFee` field with the fee details (`fee`, `fixedFee`, `networkFee`, `expectedNetworkFee`).

**Note:** Minimum invoice amounts may apply when the merchant pays fees. Contact PayKilla support for specific limits.

### Redirect URLs

Configure where to redirect users after different payment outcomes using the `urls` array.

**URL object structure:**

| Field          | Type    | Required | Description                                                             |
| -------------- | ------- | -------- | ----------------------------------------------------------------------- |
| `type`         | string  | Yes      | URL type: `SUCCESS`, `CANCEL`, or `RETURN`                              |
| `url`          | string  | Yes      | Redirect destination URL                                                |
| `autoRedirect` | boolean | No       | Auto-redirect after payment (only for `SUCCESS` type, default: `false`) |

**URL types:**

| Type      | Description                                                           |
| --------- | --------------------------------------------------------------------- |
| `SUCCESS` | Redirect destination after successful payment                         |
| `CANCEL`  | Redirect when user clicks "Cancel" button                             |
| `RETURN`  | Redirect when user clicks "Return to store" before completing payment |

**Important:**

* If a URL type is not provided, the corresponding button will **not be displayed** on the payment widget
* URLs should be valid HTTPS endpoints on your domain
* `autoRedirect` only works with `SUCCESS` type

**Redirect behavior:**

| Scenario                                                       | Button shown      | Behavior                      |
| -------------------------------------------------------------- | ----------------- | ----------------------------- |
| Invoice `PROCESSING`, no `RETURN` URL                          | No button         | User stays on payment page    |
| Invoice `PROCESSING`, has `RETURN` URL                         | "Return to store" | Redirects to `RETURN` URL     |
| Invoice `SUCCESSFUL`, no `SUCCESS` URL                         | No button         | User stays on success page    |
| Invoice `SUCCESSFUL`, has `SUCCESS` URL, `autoRedirect: false` | "Return to store" | User clicks to redirect       |
| Invoice `SUCCESSFUL`, has `SUCCESS` URL, `autoRedirect: true`  | —                 | Auto-redirect after 5 seconds |

**Auto-redirect logic:**

* When `autoRedirect: true` and invoice status changes from `PROCESSING` to `SUCCESSFUL`:
  * Shows "The payment was successful!" screen
  * Automatically redirects to `SUCCESS` URL after 5 seconds
* If user opens the widget and invoice is already `SUCCESSFUL`:
  * No auto-redirect occurs
  * Button redirects to `SUCCESS` URL on click

**Example: Full redirect configuration**

```typescript
const invoice = await createInvoice({
  type: "FIAT_BASED",
  purpose: "Order 12345",
  currency: "USD",
  totalPrice: "100",
  paymentCurrencies: ["USDTTRC"],
  urls: [
    {
      type: "SUCCESS",
      url: "https://myshop.com/order/12345/success",
      autoRedirect: true
    },
    {
      type: "CANCEL",
      url: "https://myshop.com/order/12345/cancelled"
    },
    {
      type: "RETURN",
      url: "https://myshop.com/cart"
    }
  ]
})
```

**Example: Simple success redirect**

```typescript
const invoice = await createInvoice({
  type: "FIAT_BASED",
  purpose: "Order 12345",
  currency: "USD",
  totalPrice: "100",
  paymentCurrencies: ["USDTTRC"],
  urls: [
    {
      type: "SUCCESS",
      url: "https://myshop.com/thank-you"
      // autoRedirect defaults to false — user clicks button to return
    }
  ]
})
```

### Items Structure

If you want to display itemized details on the invoice:

```typescript
const invoice = await createInvoice({
  type: "FIAT_BASED",
  purpose: "Order 12345",
  currency: "USD",
  totalPrice: "36",  // Total: 12 * 2 + 12 * 1 = 36
  paymentCurrencies: ["BTC", "ETH", "USDTTRC"],
  items: [
    {
      name: "Product A",
      price: "12",
      quantity: 2
    },
    {
      name: "Product B", 
      price: "12",
      quantity: 1
    }
  ]
})
```

| Field      | Type   | Description                                                  |
| ---------- | ------ | ------------------------------------------------------------ |
| `name`     | string | Item name (max 60 characters, must be unique within invoice) |
| `price`    | string | Price per unit (decimal with 0-2 decimal places)             |
| `quantity` | number | Quantity (integer, min: 1, max: 2,147,483,647)               |

**Validation rules:**

* Item names must be **unique** within the same invoice
* Price must be a valid decimal string (e.g., `"10"`, `"10.5"`, `"10.99"`)
* If `totalPrice` is not provided, it will be calculated from items

`quantity` is accepted both as a number (`2`) and as a string (`"2"`); in the response it is always a number. `price` comes back as a decimal string with trailing zeros (for example `"5.0000000000000000000000"`), so compare it as a number, not as text.

### Invoice Types

| Type           | When to Use                                  | Example                                   |
| -------------- | -------------------------------------------- | ----------------------------------------- |
| `FIAT_BASED`   | You want to receive a specific fiat amount   | "Charge customer $100, accept any crypto" |
| `FIXED_AMOUNT` | You want to receive a specific crypto amount | "Receive exactly 0.1 BTC"                 |
| `OPEN_AMOUNT`  | Customer chooses the amount to pay           | "Donation or top-up with any amount"      |

**How to choose:**

```typescript
// Use Case 1: E-commerce store with USD prices
// Customer buys product for $99.99 → FIAT_BASED
const storeInvoice = await createInvoice({
  type: "FIAT_BASED",
  purpose: "Order 1001",
  currency: "USD",
  totalPrice: "99.99",
  paymentCurrencies: ["BTC", "ETH", "USDTTRC"],
})

// Use Case 2: Crypto-native service charging in BTC
// Subscription costs 0.001 BTC → FIXED_AMOUNT
const subscriptionInvoice = await createInvoice({
  type: "FIXED_AMOUNT",
  purpose: "Monthly subscription",
  currency: "BTC",
  totalPrice: "0.001",
  paymentCurrencies: ["BTC"],
})

// Use Case 3: Donations or flexible top-ups
// Customer decides how much to pay → OPEN_AMOUNT
const donationInvoice = await createInvoice({
  type: "OPEN_AMOUNT",
  currency: "USD",
  purpose: "Donation",
  paymentCurrencies: ["BTC", "ETH", "USDTTRC"],
  // totalPrice not required
})
```

### Invoice Statuses

| Status             | Description                                          |
| ------------------ | ---------------------------------------------------- |
| `AWAITING_PAYMENT` | Invoice created, waiting for payment                 |
| `PROCESSING`       | Payment received, being processed (confirming, etc.) |
| `SUCCESSFUL`       | Payment completed successfully (final status)        |
| `FAILED`           | Payment failed (e.g., transaction error)             |
| `EXPIRED`          | Invoice expired without payment                      |
| `CANCELLED`        | Invoice was manually cancelled                       |
| `HOLD`             | Invoice is on hold (requires review)                 |

The `status` filter of `GET /api/v2/invoice` accepts a longer list of values: `NEW`, `AWAITING_PAYMENT`, `PROCESSING`, `FAILED`, `SUCCESSFUL`, `EXPIRED`, `CANCELLED`, `HOLD`, `REFUNDED`, `VALIDATING`, `SENDING`. The same list is used by the withdrawal and exchange lists, so not every value applies to invoices.

### Available Payment Currencies

```typescript
const VALID_CURRENCIES = [
  "BTC",      // Bitcoin
  "ETH",      // Ethereum
  "TRX",      // Tron
  "BNBBSC",   // BNB (BSC)
  "USDTETH",  // USDT (Ethereum)
  "USDTTRC",  // USDT (Tron) — recommended
  "USDTBSC",  // USDT (BSC)
]
```

This is only a short list of common tickers. Put network-qualified tickers (for example `USDTTRC`) into `paymentCurrencies`. The up-to-date list is returned by `GET /api/v2/currency` (see below) and described in [Supported Currencies](https://paykilla.gitbook.io/paykilla-docs/api-integration/supported-currencies).

### Currency List

```
GET /api/v2/currency
```

Returns an array with every currency known to PayKilla, including the ones that are currently switched off. Check the flags before you offer a currency to your customers.

```json
[
  {
    "name": "Tether",
    "ticker": "USDTTRC",
    "tickerPublic": "USDT",
    "chain": "TRX",
    "network": "Tron",
    "tokenStandard": "TRC20",
    "precision": 6,
    "balancePrecision": 4,
    "extraName": null,
    "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "isFiat": false,
    "isStable": true,
    "addressUrl": "https://tronscan.org/#/address/$$",
    "transactionUrl": "https://tronscan.org/#/transaction/$$",
    "isEnabled": true,
    "depositEnabled": true,
    "exchangeFromEnabled": true,
    "exchangeToEnabled": true,
    "withdrawalEnabled": true,
    "pairedTo": ["BTC", "ETH", "TRX"],
    "convertibleTo": [
      { "ticker": "BTC", "minFrom": "10", "maxFrom": null }
    ],
    "depositMin": "10",
    "depositMax": "500000",
    "invoiceMin": "10",
    "invoiceMax": "500000",
    "withdrawalMin": "3",
    "withdrawalMax": null
  }
]
```

`pairedTo` and `convertibleTo` are shortened in this example.

| Field                                                                             | Type           | Description                                                                                                    |
| --------------------------------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------- |
| `ticker`                                                                          | string         | Ticker used in API requests (`USDTTRC`)                                                                        |
| `tickerPublic`                                                                    | string         | Ticker without the network (`USDT`)                                                                            |
| `name`                                                                            | string         | Currency name                                                                                                  |
| `chain`, `network`                                                                | string         | Blockchain (`TRX` / `Tron`, `ETH` / `Ethereum`, `BSC` / `BinanceSmartChain`, `BTC` / `Bitcoin`, `TON` / `TON`) |
| `tokenStandard`                                                                   | string \| null | `TRC20`, `ERC20`, `BEP20`, `JETTON` (tokens on TON), or `null` for native coins and fiat currencies            |
| `contract`                                                                        | string \| null | Token contract address, `null` for native coins                                                                |
| `precision`, `balancePrecision`                                                   | number         | Number of decimal places                                                                                       |
| `extraName`                                                                       | string \| null | `null` for every currency at the time of writing                                                               |
| `isFiat`, `isStable`                                                              | boolean        | Fiat currency (`USD`, `EUR`) / stablecoin                                                                      |
| `addressUrl`, `transactionUrl`                                                    | string         | Block explorer link templates; `$$` is the placeholder for the address or the transaction hash                 |
| `isEnabled`                                                                       | boolean        | Whether the currency is switched on                                                                            |
| `depositEnabled`, `withdrawalEnabled`, `exchangeFromEnabled`, `exchangeToEnabled` | boolean        | Whether the operation is available for this currency                                                           |
| `pairedTo`                                                                        | string\[]      | Tickers this currency is paired with                                                                           |
| `convertibleTo`                                                                   | object\[]      | Exchange directions: `ticker`, `minFrom`, `maxFrom` (`null` in all entries at the time of writing)             |
| `depositMin`, `depositMax`                                                        | string         | Deposit limits, in units of the currency                                                                       |
| `invoiceMin`, `invoiceMax`                                                        | string         | Invoice amount limits, in units of the currency                                                                |
| `withdrawalMin`, `withdrawalMax`                                                  | string \| null | Withdrawal limits, in units of the currency (`withdrawalMax` is `null` at the time of writing)                 |

### Example Request

```typescript
const invoice = await createInvoice({
  type: "FIAT_BASED",
  purpose: "Order 12345",
  currency: "USD",
  totalPrice: "100",
  paymentCurrencies: ["BTC", "ETH", "USDTTRC"],
  description: "Payment for order 12345",
  expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
  clientOrderId: "order-123",  // Optional: your unique order identifier
  // Fee configuration (optional, defaults to true)
  // userPaysServiceFee: true,  // Payer pays service fee
  // userPaysNetworkFee: true,  // Payer pays network fee
  // Redirect URLs (optional)
  urls: [
    { type: "SUCCESS", url: "https://myshop.com/order/123/success" },
    { type: "RETURN", url: "https://myshop.com/cart" }
  ]
})
```

### Example Response

```json
{
  "id": "7aND5mZHW3GkRf9UbC",
  "clientOrderId": "order-123",
  "status": "AWAITING_PAYMENT",
  "type": "FIAT_BASED",
  "source": "API",
  "companyName": "My Shop Inc.",
  "expectedAmount": "100",
  "receivedAmount": "0",
  "currency": "USD",
  "purpose": "Order 12345",
  "description": "Payment for order 12345",
  "expiredAt": "2025-12-11T15:00:00.000Z",
  "createdAt": "2025-12-10T15:00:00.000Z",
  "updatedAt": "2025-12-10T15:00:00.000Z",
  "urls": [
    {
      "type": "SUCCESS",
      "url": "https://myshop.com/order/123/success",
      "autoRedirect": false
    },
    {
      "type": "RETURN",
      "url": "https://myshop.com/cart",
      "autoRedirect": false
    }
  ],
  "invoicePaymentMethods": [
    {
      "id": "Qm4T8sWzK2xVb7NcLp",
      "currency": "BTC",
      "convertedCurrency": "USD",
      "convertedRate": "85859.62332725391",
      "address": null,
      "isEnabled": true,
      "expiredAt": null
    },
    {
      "id": "Hf9RtY3uJd6GeA1oXw",
      "currency": "ETH",
      "convertedCurrency": "USD",
      "convertedRate": "2737.045683285416",
      "address": null,
      "isEnabled": true,
      "expiredAt": null
    },
    {
      "id": "Zc5MnB8vLk2PqS7dFg",
      "currency": "USDTTRC",
      "convertedCurrency": "USD",
      "convertedRate": "1",
      "address": null,
      "isEnabled": true,
      "expiredAt": null
    }
  ],
  "invoiceItems": [],
  "invoicePayments": [],
  "orderOperationFee": {
    "fee": "1.5",
    "fixedFee": "0",
    "networkFee": "0",
    "expectedNetworkFee": "0"
  },
  "restrictedPaymentMethods": [],
  "triggeredConversions": [],
  "meta": {}
}
```

Identifiers, names and amounts are sample values. `GET /api/v2/invoice/{id}` returns the same object without the `restrictedPaymentMethods` field.

* Invoice IDs and payment method IDs are 18-character strings.
* `convertedRate` is the price of one unit of the payment currency in the invoice currency (above: 1 BTC = 85,859.62 USD).
* If `clientOrderId` was not provided, it is `null` in the response.
* `orderOperationFee.fee` is the service fee rate in percent.

> **Important:** Right after creation the `address` field of every entry in `invoicePaymentMethods` is `null` — also when the invoice has a single payment currency. It stays `null` until a payment address is requested for that payment method. If you build your own checkout, see [Getting a Payment Address](#getting-a-payment-address-direct-address-flow).

### TypeScript Interfaces

Copy these interfaces to your project for type-safe API integration:

```typescript
// Request types
interface CreateInvoiceRequest {
  type: "FIAT_BASED" | "FIXED_AMOUNT" | "OPEN_AMOUNT"
  purpose: string
  currency: string
  totalPrice?: string  // Required unless items provided or type is OPEN_AMOUNT
  paymentCurrencies: string[]
  description?: string
  expiredAt?: string
  payerEmail?: string
  items?: InvoiceItem[]
  clientOrderId?: string  // Your unique order identifier (1-255 chars)
  // Fee configuration (who pays the fees)
  userPaysServiceFee?: boolean  // Default: true (payer pays)
  userPaysNetworkFee?: boolean  // Default: true (payer pays)
  // Redirect URLs
  urls?: InvoiceUrl[]
}

interface InvoiceUrl {
  type: "SUCCESS" | "CANCEL" | "RETURN"
  url: string
  autoRedirect?: boolean  // Only for SUCCESS type, default: false
}

interface InvoiceItem {
  name: string
  price: string
  quantity: number | string
}

// Response types
interface Invoice {
  id: string
  type: "FIAT_BASED" | "FIXED_AMOUNT" | "OPEN_AMOUNT"
  status: "AWAITING_PAYMENT" | "PROCESSING" | "SUCCESSFUL" | "FAILED" | "EXPIRED" | "CANCELLED" | "HOLD"
  source: "WEB" | "API"
  companyName: string
  currency: string
  expectedAmount: string
  receivedAmount: string
  purpose: string
  description: string | null
  clientOrderId: string | null
  orderOperationFee: OrderOperationFee
  createdAt: string
  updatedAt: string
  expiredAt: string | null
  urls: InvoiceUrl[]
  invoicePaymentMethods: PaymentMethod[]
  invoiceItems: InvoiceItemResponse[]
  invoicePayments: InvoicePayment[]
  restrictedPaymentMethods?: unknown[]  // only in the response of POST /api/v2/invoice
  triggeredConversions: unknown[]
  meta: { version?: string }
}

interface OrderOperationFee {
  fee: string              // Service fee rate, in percent
  fixedFee: string         // Fixed fee amount
  networkFee: string       // Network fee
  expectedNetworkFee: string  // Estimated network fee at creation
}

interface PaymentMethod {
  id: string
  currency: string
  convertedCurrency: string | null
  convertedRate: string
  address: string | null   // null until an address is requested for this method
  isEnabled: boolean
  expiredAt: string | null // set together with the address
}

interface InvoiceItemResponse {
  id: string
  name: string
  price: string      // decimal string with trailing zeros, e.g. "5.0000000000000000000000"
  quantity: number
}

interface InvoicePayment {
  id: string
  status: string  // payment status, e.g. "UNCONFIRMED", "CONFIRMED", "SUCCESSFUL"
  currency: string
  convertedCurrency: string | null
  amount: string
  convertedAmount: string
  convertedRate: string
  address: string
  hash: string
  confirmations: number
  createdAt: string
}
```

***

## Getting Invoice Details

Retrieve the current status and details of an invoice.

### Endpoint

```
GET /api/v2/invoice/{id}
```

### Example

```typescript
async function getInvoice(invoiceId: string) {
  const timestamp = Date.now()
  const recvWindow = 5000

  const params = { timestamp, recvWindow }
  const dataToSign = qs.stringify(params)
  const signature = sign(dataToSign, process.env.PAYKILLA_V2_SECRET_KEY!)

  const response = await client.get(`/api/v2/invoice/${invoiceId}`, {
    params: { ...params, signature }
  })

  return response.data
}

// Usage
const invoice = await getInvoice("7aND5mZHW3GkRf9UbC")

if (invoice.status === "SUCCESSFUL") {
  console.log("Payment completed!")
} else if (invoice.status === "EXPIRED") {
  console.log("Invoice expired")
} else {
  console.log("Still processing...")
}
```

### Response

Returns the `Invoice` object (see Example Response and TypeScript Interfaces above).

If the invoice does not exist, the API answers `404`:

```json
{
  "message": "Order operation with display ID 7aND5mZHW3GkRf9UbC was not found",
  "error": "Not Found",
  "statusCode": 404
}
```

***

## Getting an Invoice by Client Order ID

If you passed `clientOrderId` when creating the invoice, you can fetch the invoice by that value:

```
GET /api/v2/invoice/client-order-id/{clientOrderId}
```

| Parameter       | Type   | Description                                              |
| --------------- | ------ | -------------------------------------------------------- |
| `clientOrderId` | string | The `clientOrderId` you passed when creating the invoice |

```typescript
async function getInvoiceByClientOrderId(clientOrderId: string) {
  const timestamp = Date.now()
  const recvWindow = 5000

  const params = { timestamp, recvWindow }
  const signature = sign(qs.stringify(params), process.env.PAYKILLA_V2_SECRET_KEY!)

  const response = await client.get(
    `/api/v2/invoice/client-order-id/${encodeURIComponent(clientOrderId)}`,
    { params: { ...params, signature } }
  )

  return response.data
}
```

Returns the same `Invoice` object as `GET /api/v2/invoice/{id}`. Invoices created without a `clientOrderId` cannot be found this way.

If no invoice has this `clientOrderId`, the API answers `404` with the message `Order operation with clientOrderId {clientOrderId} was not found`.

***

## Listing Invoices

```
GET /api/v2/invoice
```

Returns a paginated list of your invoices, newest first.

| Parameter            | Type   | Description                                                                                               |
| -------------------- | ------ | --------------------------------------------------------------------------------------------------------- |
| `page`               | number | Page number, starting from 1                                                                              |
| `pageSize`           | number | Items per page. Default: 50, maximum: 100                                                                 |
| `status`             | string | Invoice status                                                                                            |
| `invoiceType`        | string | Invoice type: `FIAT_BASED`, `FIXED_AMOUNT`, `OPEN_AMOUNT`                                                 |
| `source`             | string | How the invoice was created: `WEB` or `API`                                                               |
| `currency`           | string | Invoice currency ticker (use a ticker returned by `GET /api/v2/currency`)                                 |
| `search`             | string | Finds an invoice by its ID or by your `clientOrderId`. Allowed characters: letters, digits, `.`, `_`, `-` |
| `dateFrom`, `dateTo` | string | ISO 8601 date or date-time, for example `2026-01-01` or `2026-01-01T00:00:00.000Z`                        |
| `orderBy`            | string | `createdAt` (the only supported value)                                                                    |
| `orderDirection`     | string | `DESC` (default) or `ASC`                                                                                 |

All parameters are optional. Invalid values of `page`, `pageSize`, `status`, `source`, `dateFrom`, `orderBy`, `orderDirection` and `search` are rejected with `400` and a message that names the parameter and the allowed values. A `currency` ticker that does not exist is answered with `500`.

**Note:** there is no `clientOrderId` filter — a `clientOrderId` parameter is ignored and the full list is returned. To find an invoice by your order identifier, use `search` or `GET /api/v2/invoice/client-order-id/{clientOrderId}`. Likewise an unknown `invoiceType` value is not rejected: the list comes back unfiltered.

```typescript
const result = await signedGet("/api/v2/invoice", { page: 1, pageSize: 20, status: "SUCCESSFUL" })
```

### Response

```json
{
  "metadata": {
    "page": 1,
    "pageSize": 20,
    "size": 87,
    "totalPages": 5
  },
  "data": [
    {
      "id": "7aND5mZHW3GkRf9UbC",
      "status": "SUCCESSFUL",
      "source": "API",
      "type": "FIAT_BASED",
      "companyName": "My Shop Inc.",
      "expectedAmount": "100",
      "receivedAmount": "100",
      "currency": "USD",
      "purpose": "Order 12345",
      "description": null,
      "expiredAt": "2025-12-11T15:00:00.000Z",
      "createdAt": "2025-12-10T15:00:00.000Z",
      "updatedAt": "2025-12-10T15:20:00.000Z"
    }
  ]
}
```

| `metadata` field | Description           |
| ---------------- | --------------------- |
| `page`           | Current page          |
| `pageSize`       | Items per page        |
| `size`           | Total number of items |
| `totalPages`     | Total number of pages |

List items are shorter than the full invoice object: they do not contain `clientOrderId`, `urls`, `invoicePaymentMethods`, `invoiceItems`, `invoicePayments`, `orderOperationFee`, `triggeredConversions` or `meta`. To read them, request the invoice by ID.

***

## Getting a Payment Address (Direct Address Flow)

Use this flow when you show the payment details in your own checkout instead of redirecting the customer to the hosted page (`https://gopay.paykilla.com/{invoice_id}`).

**The response to the create request contains no payment addresses.** Every entry of `invoicePaymentMethods` describes one currency the invoice can be paid in, and its `address` is `null` until you request an address for that payment method:

1. Create the invoice (`POST /api/v2/invoice`).
2. Take the `id` of the entry in `invoicePaymentMethods` whose `currency` the customer chose.
3. Call `GET /api/v2/invoice/payment-method/{id}` — the response contains the `address` to pay to.

```
GET /api/v2/invoice/payment-method/{id}
```

| Parameter | Type   | Description                                         |
| --------- | ------ | --------------------------------------------------- |
| `id`      | string | Payment method ID from `invoicePaymentMethods[].id` |

```typescript
async function getPaymentMethod(paymentMethodId: string) {
  const timestamp = Date.now()
  const recvWindow = 5000

  const params = { timestamp, recvWindow }
  const signature = sign(qs.stringify(params), process.env.PAYKILLA_V2_SECRET_KEY!)

  const response = await client.get(
    `/api/v2/invoice/payment-method/${paymentMethodId}`,
    { params: { ...params, signature } }
  )

  return response.data
}

// Usage: the customer chose USDT (TRC-20)
const method = invoice.invoicePaymentMethods.find((m) => m.currency === "USDTTRC")
if (!method) throw new Error("USDTTRC is not available for this invoice")
const { address, expiredAt } = await getPaymentMethod(method.id)
```

### Response

```json
{
  "id": "Zc5MnB8vLk2PqS7dFg",
  "currency": "USDTTRC",
  "convertedCurrency": "USD",
  "convertedRate": "1",
  "address": "TXYZabcdefghijklmnopqrstuvwxyz1234",
  "isEnabled": true,
  "expiredAt": "2025-12-10T15:15:19.462Z"
}
```

| Field               | Type           | Description                                                                                                                      |
| ------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `id`                | string         | Payment method ID                                                                                                                |
| `currency`          | string         | Ticker the customer pays in                                                                                                      |
| `convertedCurrency` | string \| null | Invoice currency ticker (for cross-currency invoices)                                                                            |
| `convertedRate`     | string         | Price of one unit of `currency` in `convertedCurrency`                                                                           |
| `address`           | string         | Address to pay to                                                                                                                |
| `isEnabled`         | boolean        | Whether the method can be used                                                                                                   |
| `expiredAt`         | string         | Expiration time of this payment method: 15 minutes after the first request for the address. It is not extended by later requests |

When you read the invoice, the entries of `invoicePaymentMethods` that already have an address show `address` and `expiredAt`; all other entries have `null` in both fields.

If the ID is unknown, the API answers `404` with the message `Payment methods for invoice were not found`.

Repeating the call does not give you a new address or a new rate: it returns the same `address`, the same `expiredAt` and the same `convertedRate` — also after `expiredAt` has passed.

***

## Important Restrictions

### Forbidden Characters

PayKilla **does not accept** some special characters in free-text fields (this does not apply to `clientOrderId`, which has its own rules above). A `purpose` with a dash is rejected with `400` and the message `Purpose contains invalid characters`; `items[].name` is checked against an allowed-character pattern as well. To be safe, clean `description` the same way:

```typescript
// ❌ Incorrect
purpose: "Order 12345 - blue edition"  // dash is forbidden

// ✅ Correct
purpose: "Order 12345 blue edition"    // no dash
```

**String sanitization function:**

```typescript
function cleanString(str: string): string {
  return str
    .replace(/[-–—]/g, " ")      // Replace dashes with spaces
    .replace(/[^\w\s.,]/g, "")   // Remove special characters
    .replace(/\s+/g, " ")        // Remove extra spaces
    .trim()
}

// Apply to fields:
const cleanPurpose = cleanString(purpose)
const cleanDescription = cleanString(description)
const cleanItemName = cleanString(item.name)
```

Note that this function also removes every non-ASCII letter (Cyrillic, accented characters).

### Invalid Payment Currencies

Use only valid tickers:

```typescript
// ❌ Incorrect
paymentCurrencies: ["USDT"]  // USDT without network specification

// ✅ Correct
paymentCurrencies: ["USDTTRC", "USDTETH"]  // With network specification
```

***

## Webhooks

### Endpoint Configuration

Set up your webhook endpoint in the PayKilla dashboard (**Settings → Webhooks**): enter the URL and save it, then attach an API key to the endpoint (its secret key signs the deliveries) and select the events. The URL is yours — any public `https://` endpoint, for example:

```
https://your-domain.com/webhooks/paykilla
```

### Webhook Events

| Event                   | Priority | Description                                |
| ----------------------- | -------- | ------------------------------------------ |
| `PAYMENT_COMPLETED`     | HIGH     | Payment completed successfully             |
| `PAYMENT_FAILED`        | HIGH     | Payment failed                             |
| `PAYMENT_PENDING`       | LOW      | Payment address requested, payment awaited |
| `TRANSACTION_CONFIRMED` | MEDIUM   | Transaction confirmed                      |
| `INVOICE_CREATED`       | LOW      | Invoice created                            |
| `INVOICE_EXPIRED`       | MEDIUM   | Invoice expired                            |
| `INVOICE_PAID`          | MEDIUM   | Invoice paid                               |

For a complete list of all webhook events, see the [Webhooks Documentation](https://paykilla.gitbook.io/paykilla-docs/api-integration/webhooks).

### Webhook Signature Verification

Webhooks are signed using HMAC-SHA256. The signature is computed over: `timestamp + method + url + body`, where `url` is the full URL of your webhook endpoint.

```typescript
import { createHmac, timingSafeEqual } from "crypto"

const SECRET_KEY = process.env.PAYKILLA_V2_SECRET_KEY!
const WEBHOOK_URL = "https://your-domain.com/webhooks/paykilla" // exactly as saved in the dashboard

// In the webhook handler
export async function POST(request: Request) {
  const timestamp = request.headers.get("X-API-TIMESTAMP")
  const recvWindow = parseInt(request.headers.get("X-API-RECV-WINDOW") || "5000", 10)
  const signature = request.headers.get("X-API-SIGN")
  const payload = await request.text()

  // Verify timestamp freshness
  if (!timestamp || !signature || Date.now() - parseInt(timestamp, 10) > recvWindow) {
    return new Response("Invalid signature", { status: 401 })
  }

  // Verify HMAC signature
  const message = `${timestamp}POST${WEBHOOK_URL}${payload}`
  const expected = createHmac("sha256", SECRET_KEY).update(message).digest("hex")
  
  // timingSafeEqual throws on buffers of different length, so compare the length first
  const expectedBuf = Buffer.from(expected, "hex")
  const signatureBuf = Buffer.from(signature, "hex")
  const isValid = expectedBuf.length === signatureBuf.length && timingSafeEqual(expectedBuf, signatureBuf)

  if (!isValid) {
    return new Response("Invalid signature", { status: 401 })
  }

  const event = JSON.parse(payload)

  switch (event.eventType) {
    case "PAYMENT_COMPLETED":
      await handlePaymentComplete(event.data)
      break
    case "INVOICE_EXPIRED":
      await handlePaymentExpired(event.data)
      break
  }

  return new Response("OK", { status: 200 })
}
```

For more details on signature verification, see the [Webhooks Documentation](https://paykilla.gitbook.io/paykilla-docs/api-integration/webhooks#signature-verification).

***

## Complete Integration Example

### Backend API Route

```typescript
// app/api/paykilla/create-invoice/route.ts
import { NextResponse } from "next/server"
import { createHmac } from "crypto"
import axios from "axios"
import qs from "qs"

const API_KEY = process.env.PAYKILLA_V2_API_KEY!
const SECRET_KEY = process.env.PAYKILLA_V2_SECRET_KEY!
const BASE_URL = "https://account-api.paykilla.com"
const WIDGET_URL = "https://gopay.paykilla.com"

const client = axios.create({
  baseURL: BASE_URL,
  headers: { "X-API-KEY": API_KEY },
})

function sign(data: string): string {
  return createHmac("sha256", SECRET_KEY).update(data).digest("hex")
}

function cleanString(str: string): string {
  return str
    .replace(/[-–—]/g, " ")
    .replace(/[^\w\s.,]/g, "")
    .replace(/\s+/g, " ")
    .trim()
}

export async function POST(request: Request) {
  const body = await request.json()

  const timestamp = Date.now()
  const recvWindow = 5000
  const dataToSign = qs.stringify({ timestamp, recvWindow })
  const signature = sign(dataToSign)

  // Determine invoice type
  const FIAT_CURRENCIES = ["USD", "EUR"]
  const invoiceType = FIAT_CURRENCIES.includes(body.currency)
    ? "FIAT_BASED"
    : "FIXED_AMOUNT"

  // Prepare data
  const invoiceData = {
    type: invoiceType,
    purpose: cleanString(body.purpose),
    currency: body.currency,
    totalPrice: String(body.amount),
    paymentCurrencies: ["BTC", "ETH", "USDTTRC"],
    description: body.description ? cleanString(body.description) : undefined,
    expiredAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
  }

  try {
    const response = await client.post("/api/v2/invoice", invoiceData, {
      params: { timestamp, recvWindow, signature }
    })

    return NextResponse.json({
      invoice: {
        id: response.data.id,
        url: `${WIDGET_URL}/${response.data.id}`,
        status: response.data.status,
      }
    })
  } catch (error) {
    console.error("PayKilla API Error:", error)
    return NextResponse.json(
      { error: "Failed to create invoice" },
      { status: 500 }
    )
  }
}
```

### Frontend Component

```tsx
// components/PayButton.tsx
"use client"

import { useState } from "react"

interface PayButtonProps {
  amount: number
  currency: string
  purpose: string
  description?: string
}

export function PayButton({ amount, currency, purpose, description }: PayButtonProps) {
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  const handlePay = async () => {
    setLoading(true)
    setError(null)

    try {
      const response = await fetch("/api/paykilla/create-invoice", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ amount, currency, purpose, description }),
      })

      if (!response.ok) {
        throw new Error("Failed to create invoice")
      }

      const data = await response.json()

      // Redirect to payment page
      window.location.href = data.invoice.url
    } catch (err) {
      setError(err instanceof Error ? err.message : "Unknown error")
    } finally {
      setLoading(false)
    }
  }

  return (
    <div>
      <button
        onClick={handlePay}
        disabled={loading}
        className="bg-green-600 text-white px-6 py-3 rounded-lg"
      >
        {loading ? "Creating Payment..." : "Pay with Crypto"}
      </button>
      {error && <p className="text-red-500 mt-2">{error}</p>}
    </div>
  )
}
```

### Usage

```tsx
<PayButton
  amount={100}
  currency="USD"
  purpose="Order 12345"
  description="Payment for order 12345"
/>
```

***

## Troubleshooting

The format of the error body and the authentication errors are described in [Response Format](#response-format).

### Invalid Signature (400)

**Cause:** The `signature` parameter does not match (`"message": "Invalid signature"`).

**Solution:**

1. Make sure you are using HMAC-SHA256 with your secret key and send the result as a hex string
2. For POST requests, sign only `timestamp` + `recvWindow`
3. For GET requests, sign all query parameters in the order in which you send them, followed by `timestamp` and `recvWindow`
4. For GET requests, sign the raw parameter values, not their URL-encoded form (see [Requests with Query Parameters](#requests-with-query-parameters))

### Timestamp outside recvWindow (400)

**Cause:** The `timestamp` is older than `recvWindow` milliseconds when the request reaches the server.

**Solution:** Generate `timestamp` immediately before sending each request and keep your server clock synchronized.

### Forbidden Resource (403)

**Cause:** The `X-API-KEY` header or the `signature` parameter is missing.

**Solution:**

1. Check the `X-API-KEY` header (it must contain the public key)
2. Check that `timestamp`, `recvWindow` and `signature` are passed in the query string
3. Check the permissions of the API key in the dashboard (**Settings → API keys**)

### Not Found (404)

**Cause:** The requested object or the path does not exist. The `message` tells which:

| `message`                                                          | Meaning                              |
| ------------------------------------------------------------------ | ------------------------------------ |
| `Order operation with display ID {id} was not found`               | No invoice with this ID              |
| `Order operation with clientOrderId {clientOrderId} was not found` | No invoice with this `clientOrderId` |
| `Payment methods for invoice were not found`                       | No payment method with this ID       |
| `Cannot GET /api/v2/...`                                           | The path itself does not exist       |

### Invalid Characters (400)

**Cause:** Special characters in a free-text field, for example a dash in `purpose` (`"message": "Purpose contains invalid characters"`).

**Solution:**

```typescript
purpose = purpose.replace(/[-–—]/g, " ").replace(/[^\w\s.,]/g, "")
```

### Validation Errors (400)

**Cause:** A field has a wrong value. `message` is an array that names the field and the allowed values, for example:

```json
{
  "message": ["type must be one of the following values: FIXED_AMOUNT, FIAT_BASED, OPEN_AMOUNT, MULTI_CURRENCY"],
  "error": "Bad Request",
  "statusCode": 400
}
```

### Internal Server Error (500)

**Cause:** A ticker that does not exist — in `paymentCurrencies` when creating an invoice, or in the `currency` filter of the invoice list. The API answers `500` in this case.

**Solution:** Use only tickers returned by `GET /api/v2/currency`.

### Invalid Client Order ID (400)

**Cause:** The `clientOrderId` parameter contains invalid characters (`"message": "Client order ID contains invalid characters. Allowed: A-Z, a-z, 0-9, -, _, ."`) or has incorrect length.

**Solution:**

1. Use only allowed characters: `A-Z`, `a-z`, `0-9`, `-`, `_`, `.`
2. Ensure length is between 1 and 255 characters

```typescript
// ❌ Incorrect
clientOrderId: "order#123"      // # is not allowed
clientOrderId: "order 123"      // spaces not allowed
clientOrderId: ""               // empty string not allowed

// ✅ Correct
clientOrderId: "order-123"
clientOrderId: "INV_2025_001"
clientOrderId: "abc.def_123"
```

### Duplicate Client Order ID (409)

**Cause:** An invoice with this `clientOrderId` already exists for your merchant account (`"message": "clientOrderId already exists for this merchant"`).

**Solution:**

1. Use a unique `clientOrderId` for each invoice
2. Check if an invoice with this ID already exists before creating a new one (`GET /api/v2/invoice/client-order-id/{clientOrderId}`)

***

## API Reference

### Endpoints

| Method | Endpoint                                          | Description                                    |
| ------ | ------------------------------------------------- | ---------------------------------------------- |
| GET    | `/api/v2/currency`                                | List of currencies with their flags and limits |
| POST   | `/api/v2/invoice`                                 | Create an invoice                              |
| GET    | `/api/v2/invoice`                                 | List invoices with filtering/pagination        |
| GET    | `/api/v2/invoice/{id}`                            | Get invoice details by ID                      |
| GET    | `/api/v2/invoice/client-order-id/{clientOrderId}` | Get invoice by client order ID                 |
| GET    | `/api/v2/invoice/payment-method/{id}`             | Get the payment address of a payment method    |

`PUT /api/v2/invoice/{id}/expire` and `GET /api/v2/invoice/payment-info` do not exist in API v2: both paths answer `404`. An invoice with `expiredAt` gets the status `EXPIRED` when that time passes.

### Headers

| Header         | Value              |
| -------------- | ------------------ |
| `X-API-KEY`    | Your public key    |
| `Content-Type` | `application/json` |

### Query Parameters (all requests)

| Parameter    | Description                      |
| ------------ | -------------------------------- |
| `timestamp`  | Unix timestamp in milliseconds   |
| `recvWindow` | Validity window (typically 5000) |
| `signature`  | HMAC-SHA256 signature            |

***

## Support

If you have any questions, please contact PayKilla support.
