# Node.js library

The `@payitgo/node` package wraps the payitgo API for Node.js and TypeScript: payments,
tokens, your webhook (IPN) and its signature check, and the public checkout
calls. It has no runtime dependencies and ships its own TypeScript types.
Use it on your server only: it holds your secret key.

Requests and responses use the API's own field names (`order_id`,
`checkout_url`, …), so the [Payments](/docs/payments),
[Webhooks](/docs/webhooks) and [Checkout](/docs/checkout) pages describe every
field this library returns.

| Method | HTTP | Returns |
|---|---|---|
| [`payments.create(params)`](#paymentscreate) | `POST /v1/payments` | [`Payment`](#payment) |
| [`payments.retrieve(id)`](#paymentsretrieve) | `GET /v1/payments/{id}` | [`Payment`](#payment) |
| [`payments.list(params?)`](#paymentslist) | `GET /v1/payments` | [`PaymentList`](#paymentlist) |
| [`payments.selectToken(id, token)`](#paymentsselecttoken) | `POST /v1/payments/{id}/token` | [`Payment`](#payment) |
| [`payments.simulate(id, outcome)`](#paymentssimulate) | `POST /v1/payments/{id}/simulate` | [`Payment`](#payment) |
| [`tokens.list(params?)`](#tokenslist) | `GET /v1/tokens` | [`TokenInfo[]`](#tokeninfo) |
| [`webhooks.get()`](#webhooksget) | `GET /v1/webhook` | [`Webhook`](#webhook) |
| [`webhooks.set(url)`](#webhooksset) | `PUT /v1/webhook` | [`Webhook`](#webhook) |
| [`webhooks.remove()`](#webhooksremove) | `DELETE /v1/webhook` | [`Webhook`](#webhook) |
| [`webhooks.test()`](#webhookstest) | `POST /v1/webhook/test` | [`WebhookDelivery`](#webhookdelivery) |
| [`webhooks.constructEvent(body, header, secret?, tolerance?)`](#webhooksconstructevent) | none (local) | [`WebhookEvent`](#webhookevent) |
| [`Payitgo.webhooks.constructEvent(body, header, secret, tolerance?)`](#payitgowebhooksconstructevent) | none (local, no client) | [`WebhookEvent`](#webhookevent) |
| [`checkout.retrieve(checkoutToken)`](#checkoutretrieve) | `GET /checkout/{checkout_token}` | [`Payment`](#payment) |
| [`checkout.selectToken(checkoutToken, token)`](#checkoutselecttoken) | `POST /checkout/{checkout_token}/token` | [`Payment`](#payment) |
| [`isLive()`](#islive) | `GET /v1/webhook`, the first time only | `boolean` |

Every method that calls the API returns a `Promise`.

## Install

```bash
npm install @payitgo/node
```

| Requirement | |
|---|---|
| Node.js | 18 or later (the library uses the built-in `fetch`) |
| Dependencies | None |
| TypeScript | Types included (`dist/index.d.ts`); no `@types` package needed |
| Module format | CommonJS, with an ES module entry point (package.json `exports`) |

```ts
// ES modules and TypeScript: the default export is the Payitgo class
import Payitgo, { PayitgoError, SignatureVerificationError } from "@payitgo/node";

// CommonJS
const { Payitgo, PayitgoError, SignatureVerificationError } = require("@payitgo/node");
```

Both entry points share one implementation, so `instanceof PayitgoError` holds
whichever way a module imported the package. Only the package root is public:
deep imports such as `payitgo/dist/index.js` are not exported.

## Quickstart

```ts
import { Payitgo } from "@payitgo/node";

const payitgo = new Payitgo(process.env.PAYITGO_KEY!, { baseUrl: "https://api.payitgo.co" });

const payment = await payitgo.payments.create({
  order_id: "ORD-1001",
  amount_usd: "49.90",
  token: "USDT_TRC20",
});
console.log(payment.crypto_amount, payment.token, "to", payment.deposit_address);
```

Then [set your webhook URL](#webhooksset) and fulfil the order when
[`payment.confirmed`](#a-webhook-ipn-handler-with-express) arrives.

## Configuration

### new Payitgo

```ts
new Payitgo(apiKey: string, options?: PayitgoOptions)
```

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `apiKey` | string | yes | | Your secret key, test or live ([Authentication](/docs/authentication)). An empty key throws |
| `options.baseUrl` | string | no | `PAYITGO_BASE_URL`, then the address the package was released with | The gateway's address, `https://api.payitgo.co`. Trailing slashes are removed |
| `options.timeoutMs` | number | no | `30000` | How long one attempt may take, in milliseconds, reading the answer included. A timeout counts as a network error, so it is [retried](#retries) |
| `options.maxRetries` | number | no | `2` | How many times a request is retried after a network error, a `429` or a `5xx`. `0` turns retries off |
| `options.webhookSecret` | string | no | none | Your mode's signing secret. [`webhooks.constructEvent`](#webhooksconstructevent) uses it when you pass none |
| `options.webhookUrl` | string | no | none | Sent as `webhook_url` with every [`payments.create`](#paymentscreate) that does not pass its own |

The constructor makes no request. It throws a plain `Error` (not a
`PayitgoError`) when:

| Message | Cause |
|---|---|
| `payitgo: the API key is required` | `apiKey` is missing or empty |
| `payitgo: set baseUrl (or PAYITGO_BASE_URL) to your gateway's address` | No `baseUrl`, no `PAYITGO_BASE_URL`, and no address built into the package |

```ts
const payitgo = new Payitgo(process.env.PAYITGO_KEY!, {
  baseUrl: "https://api.payitgo.co",                         // or set PAYITGO_BASE_URL
  timeoutMs: 15_000,
  maxRetries: 2,
  webhookSecret: process.env.PAYITGO_WEBHOOK_SECRET, // the signing secret of the key's mode
  webhookUrl: "https://acme.example/payitgo/ipn",    // optional default for payments.create
});
```

One client can serve your whole application: it keeps no per-request state.

### Properties

| Property | Type | Description |
|---|---|---|
| `payments` | object | [Payments](#payments) |
| `tokens` | object | [Tokens](#tokens) |
| `webhooks` | object | [Webhooks](#webhooks) |
| `checkout` | object | [Checkout](#checkout) |
| `Payitgo.webhooks` | object (static) | [`constructEvent`](#payitgowebhooksconstructevent) without a client |

The module also exports `VERSION` (`"0.1.0"`), sent in the
`User-Agent: payitgo-node/0.1.0` header, and `webhooks`, the same object as
`Payitgo.webhooks`.

### isLive

`GET /v1/webhook` · [API reference](/docs/webhooks#read-the-webhook)

```ts
payitgo.isLive(): Promise<boolean>
```

**Returns** `true` for a live key, `false` for a test key. A key does not say
its mode, so the first call asks payitgo (the `livemode` of the key's
[webhook](#webhooksget); a read-only key may call it) and the answer is kept
for the client's life: later calls make no request.

**Errors** `PayitgoError`, as [any call](#errors): `401` for a key payitgo
does not know, `403` for a live key while live mode is off. An error is not
kept: the next call asks again.

```ts
if (!(await payitgo.isLive())) console.log("test mode: payments are simulated");
```

### Base URL

The gateway address is taken from, in order: the `baseUrl` option, the
`PAYITGO_BASE_URL` environment variable, and the address the package was
released with. Setting the variable keeps the address out of your code:

```bash
export PAYITGO_BASE_URL=https://api.payitgo.co
```

### Test and live mode

The key decides the mode. A test key creates and reads test payments
(no coins: you [simulate](#paymentssimulate) what the customer sends) and
manages the test webhook; a live key does the same for live ones. The key
does not say which it is: the dashboard shows a Test or Live badge next to
it, and [`payitgo.isLive()`](#islive) asks payitgo. Pair each key with the
webhook secret of the same mode. See [Test mode](#test-mode).

### HTTP client

Requests go through Node's global `fetch`, with the headers
`Authorization: Bearer <key>`, `Accept: application/json`,
`Content-Type: application/json` (when there is a body) and
`User-Agent: payitgo-node/0.1.0`. There is no option to pass your own HTTP
client.

## Errors

### PayitgoError

Every call that reaches the API throws a `PayitgoError` when the request fails.

```ts
class PayitgoError extends Error {
  readonly status: number;
  name: "PayitgoError";
}
```

| Field | Type | Description |
|---|---|---|
| `status` | number | The HTTP status, or `0` when no full answer came (a network error, or a timeout before or while the answer was read, after the retries) |
| `message` | string | The API's `error` message, such as `minimum payment in USDT_TRC20 is $6.44 right now (network fees)`. `HTTP <status>` when the answer had no message. `payitgo: request failed: <cause>` for a network error |

Branch on `status`; log `message` (its wording may change, see
[Errors and limits](/docs/errors)).

| `status` | Meaning | Retried by the library |
|---|---|---|
| `0` | No full answer: DNS, connection, TLS, or a timeout (also while reading the answer) | Yes |
| `400` | The request is invalid (the message says which field) | No |
| `401` | `missing api key` or `invalid api key` | No |
| `403` | Not allowed, for example a live key while live mode is off, or a storefront key changing the account's webhook | No |
| `404` | No such payment in the key's mode | No |
| `409` | Conflicts with the current state, such as choosing a payment's coin twice, or simulating a payment whose simulation is running | No |
| `429` | Over the [rate limit](/docs/errors#rate-limits) | Yes |
| `500` and other `5xx` | Something failed on payitgo's side | Yes |

```ts
import { PayitgoError } from "@payitgo/node";

try {
  await payitgo.payments.create({ order_id: "ORD-1", amount_usd: 1, token: "BTC" });
} catch (err) {
  if (err instanceof PayitgoError && err.status === 400) {
    console.warn("payitgo refused:", err.message); // e.g. an amount under the coin's minimum
  } else {
    throw err;
  }
}
```

### SignatureVerificationError

[`constructEvent`](#webhooksconstructevent) throws a
`SignatureVerificationError` (a subclass of `Error`, `name`
`"SignatureVerificationError"`) when a notification cannot be trusted. Answer
`400`.

| `message` | Cause |
|---|---|
| `missing or malformed X-Webhook-Signature header` | No header, or no `t` or `v1` in it |
| `signature does not match` | The body, the timestamp or the secret differ from what payitgo signed (a `v1` that is not 64 hex characters included) |
| `malformed timestamp` | `t` is not a whole number of seconds (checked unless the tolerance is `0`) |
| `timestamp outside the tolerance` | `t` is further than the tolerance (5 minutes) from your clock |
| `no webhook secret: pass one, or set the client's webhookSecret` | The client method got no secret and the client has no `webhookSecret` |

A body that is correctly signed but is not JSON throws a `SyntaxError` from
`JSON.parse`; payitgo never sends one.

### Retries

A request is retried, up to `maxRetries` times (default 2, so at most 3
attempts), after a network error, a timeout (before or while the answer is
read), a `429` or a `5xx`. It waits
0.5 s before the first retry and 1 s before the second (500 ms × 2ⁿ). Other
statuses are never retried. When the last attempt fails, the call throws a
`PayitgoError` with that attempt's status (`0`, `429` or `5xx`).

Every call is retried the same way, and each is safe to repeat:

| Call | Repeating it |
|---|---|
| `payments.create` | Returns the order's open payment instead of a second one ([safe retries](/docs/payments#safe-retries)) |
| `payments.selectToken`, `checkout.selectToken` | If the first attempt went through, the repeat throws `409`: read the payment with `retrieve`, it has its coin |
| `payments.simulate` | If the first attempt went through, the repeat throws `409` while that simulation runs: read the payment with `retrieve` |
| `webhooks.set`, `webhooks.remove` | Changes nothing the second time |
| `webhooks.test` | Queues one more `ping` |
| Reads | Change nothing |

## Payments

### payments.create

`POST /v1/payments` · [API reference](/docs/payments#create-a-payment)

```ts
payments.create(params: CreatePaymentParams): Promise<Payment>
```

Creates a payment for one of your orders. With a `token`, the payment comes
back with its deposit address, amount, QR payload and a 15-minute deadline.
Without one, it comes back with `token_options` and the customer (or your
page, with [`selectToken`](#paymentsselecttoken)) chooses the coin. While the
order has an open payment, creating it again returns that payment.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `order_id` | string | yes | Your id for the order, 1 to 128 bytes. One open payment per order and mode |
| `amount_usd` | number or string | yes, unless `price_amount` + `price_currency` | The price in US dollars, such as `"49.90"`. Sent as you give it; a string avoids float rounding |
| `price_amount` | number or string | with `price_currency` | Instead of `amount_usd`: the price in another currency, such as `"45.50"`, which payitgo converts into dollars at the rate of the moment ([prices in other currencies](/docs/payments#prices-in-other-currencies)). Sent as you give it |
| `price_currency` | string | with `price_amount` | The ISO 4217 code of its currency, such as `"EUR"`, `"GBP"` or `"NGN"` (any case) |
| `token` | string | no | The coin, such as `"USDT_TRC20"` (from [`tokens.list`](#tokenslist)). Leave it out to let the customer choose |
| `tokens` | string[] | no | Without `token`: the coins to offer, such as `["USDT_TRC20", "USDT_BEP20"]`. Default: every coin you accept whose minimum the amount reaches |
| `description` | string | no | What the customer pays for, up to 500 bytes |
| `metadata` | `Record<string, string>` | no | Your own data: up to 20 keys of up to 40 bytes, string values of up to 500 |
| `customer_email` | string | no | The customer's email address |
| `success_url` | string | no | `http(s)` URL the hosted checkout links to once the payment is confirmed |
| `cancel_url` | string | no | `http(s)` URL the hosted checkout links back to if the payment closes unpaid |
| `webhook_url` | string | no | `https://` URL that gets this payment's notifications instead of your account's webhook URL ([per-payment URL](/docs/webhooks#per-payment-webhook-url)). Default: the client's `webhookUrl`. Pass `""` to send none for this payment, even with a client default |
| `simulate` | [`SimulationOutcome`](#simulation) | no | Test keys only: the outcome to [simulate](#paymentssimulate). With `token` it starts at once; without, once the coin is chosen |

**Returns** the [`Payment`](#payment): the new one (`201`) or the order's open
one (`200`); both resolve the same way. Fields: [the payment
object](/docs/payments#the-payment-object).

**Errors** `PayitgoError` with status `400` for an invalid request: a missing
`order_id`, `amount_usd must be > 0`, a token you do not accept, an amount
under the token's minimum, a `webhook_url` that is not public `https`, a
`simulate` with a live key or an unknown outcome, a price in another
currency given with `amount_usd`, without its pair or in a code that is no
currency, and the rest of the [create errors](/docs/payments#errors). `503`
when no exchange rate can be had for `price_currency` (after the
[retries](#retries)): try again shortly. Also `401`, `403`, `429`,
`500` and `0` as for [every call](#payitgoerror).

```ts
// The customer pays in a coin you know:
const payment = await payitgo.payments.create({
  order_id: "ORD-1001",
  amount_usd: "49.90",
  token: "USDT_TRC20",
  description: "2 x Acme T-shirt",
  metadata: { customer_id: "cus_42" },
  customer_email: "jane@example.com",
  success_url: "https://acme.example/thanks",
  cancel_url: "https://acme.example/cart",
});
// payment.deposit_address, payment.crypto_amount, payment.qr_payload, payment.expires_at

// The customer chooses the coin (on the hosted page, or yours):
const open = await payitgo.payments.create({ order_id: "ORD-1002", amount_usd: 25 });
open.token_options?.forEach((t) => console.log(t.token, t.min_usd, t.amount_due_usd ?? ""));

// This payment's notifications go to its own URL:
await payitgo.payments.create({
  order_id: "ORD-1003",
  amount_usd: 30,
  token: "USDT_TRC20",
  webhook_url: "https://acme.example/orders/ORD-1003/ipn",
});

// Priced in euros: converted into dollars at the rate of the moment.
const priced = await payitgo.payments.create({ order_id: "ORD-1004", price_amount: "45.50", price_currency: "EUR" });
// priced.price_amount "45.5", priced.price_currency "EUR", priced.price_rate "1.082", priced.usd_amount "49.23"
```

### payments.retrieve

`GET /v1/payments/{id}` · [API reference](/docs/payments#retrieve-a-payment)

```ts
payments.retrieve(id: string): Promise<Payment>
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The payment's id, or your `order_id` for the order's **open** payment (the payment id is tried first). Escaped for the URL by the library |

**Returns** the [`Payment`](#payment).

**Errors** `PayitgoError` `404` (`payment not found`) for an unknown id, a payment
of the other mode, or an order with no open payment. Plus the
[common statuses](#payitgoerror).

```ts
const byId = await payitgo.payments.retrieve("8f3a2c1e9b7d4a6f05e1c2d3");
const byOrder = await payitgo.payments.retrieve("ORD-1001"); // the order's open payment
if (byId.status === "confirmed") console.log("paid at", byId.confirmed_at);
```

### payments.list

`GET /v1/payments` · [API reference](/docs/payments#list-payments)

```ts
payments.list(params?: ListPaymentsParams): Promise<PaymentList>
```

Payments of the key's mode, newest first.

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `status` | [`PaymentStatus`](#paymentstatus) | no | all | Only payments in this [status](/docs/payments#lifecycle) |
| `chain` | string | no | all | Only payments on this network, such as `"tron"` |
| `token` | string | no | all | Only payments in this coin, such as `"USDT_TRC20"` |
| `store_id` | string | no | all | Only payments of this [store](/docs/account#stores) (ignored for a storefront key, which sees its own) |
| `limit` | number | no | `50` | Page size, 1 to 200 (outside that range the API uses 50) |
| `offset` | number | no | `0` | How many to skip |

Parameters that are `undefined`, `null` or `""` are left out of the query.

**Returns** a [`PaymentList`](#paymentlist): `payments`, `total` (matches across
all pages), and the `limit` and `offset` applied.

**Errors** the [common statuses](#payitgoerror) only. An unknown status, chain or
token matches nothing.

```ts
// Every confirmed payment, page by page:
const confirmed = [];
for (let offset = 0; ; offset += 100) {
  const page = await payitgo.payments.list({ status: "confirmed", limit: 100, offset });
  confirmed.push(...page.payments);
  if (offset + page.limit >= page.total) break;
}
```

### payments.selectToken

`POST /v1/payments/{id}/token` · [API reference](/docs/payments#choose-the-coin-later)

```ts
payments.selectToken(id: string, token: string): Promise<Payment>
```

Sets the coin of a payment created without one, once the customer picked it on
[your own page](#your-own-payment-page). The coin can be set once.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The payment's id or your `order_id` |
| `token` | string | yes | One of the payment's `token_options[].token`, such as `"USDT_TRC20"` |

**Returns** the whole [`Payment`](#payment), now with `deposit_address`,
`crypto_amount`, `rate_locked`, `qr_payload` and an `expires_at` 15 minutes
away.

**Errors** `PayitgoError`:

| `status` | `message` | Cause |
|---|---|---|
| `400` | `token is required` | Empty `token` |
| `400` | `token is not one of this payment's options` | A coin not in `token_options` |
| `400` | `minimum payment in … is $… right now (it is converted on its way to the merchant, and the exchange takes no less)` | The coin needs a bigger payment than this one: it is converted through the exchange on its way to the merchant, which takes no less, and the merchant pays the fees or hides such coins (`hide_below_minimum`). Offer the other coins |
| `400` | `this payment can no longer be paid` | The payment is no longer `pending`, or its hour to choose has passed |
| `404` | `payment not found` | No such payment in the key's mode |
| `409` | `the coin for this payment has already been chosen` | The coin was set before (by you or on the hosted page) |

```ts
const payment = await payitgo.payments.create({ order_id: "ORD-1002", amount_usd: 25 });
// …your page lists payment.token_options, the customer picks one…
const chosen = await payitgo.payments.selectToken(payment.id, "USDT_BEP20");
console.log(chosen.crypto_amount, chosen.token, chosen.deposit_address, chosen.expires_at);
```

### payments.simulate

`POST /v1/payments/{id}/simulate` · [API reference](/docs/testing#simulate-a-payment)

```ts
payments.simulate(id: string, outcome: SimulationOutcome): Promise<Payment>
```

Plays an outcome on a test payment: payitgo sends its deposits, status changes
and webhooks about 5 seconds apart, as live coins would, with no coins
([Test mode](/docs/testing)). Test keys only.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The payment's id or your `order_id` |
| `outcome` | [`SimulationOutcome`](#simulation) | yes | Such as `"confirmed"` ([what each does](/docs/testing#outcomes)) |

**Returns** the whole [`Payment`](#payment), with its `simulation` started.
Simulated deposits have `simulated: true`; their `tx_id` names no transaction on any chain.

**Errors** `PayitgoError`:

| `status` | `message` | Cause |
|---|---|---|
| `400` | `outcome is required` | Empty `outcome` |
| `400` | `simulate must be one of: confirmed, overpaid, …` | An unknown outcome |
| `400` | `only test payments can be simulated: use a test key` | A live key |
| `400` | `choose the payment's coin first` | No coin yet, for anything but `expired` |
| `400` | `this payment is confirmed: only a pending or partially paid payment can be simulated` | The payment is closed or confirming |
| `400` | `a BTC address only receives BTC: wrong_asset needs a coin on Tron, Ethereum or BNB Smart Chain` | `wrong_asset` on a coin that has no other |
| `404` | `payment not found` | No such payment in the key's mode |
| `409` | `a simulation is already running for this payment` | Wait until its `state` is `"done"` |

```ts
const payment = await payitgo.payments.simulate("ORD-1002", "partially_paid");
console.log(payment.simulation); // { outcome: "partially_paid", state: "running" }
// once it is done, pay the rest:
await payitgo.payments.simulate("ORD-1002", "confirmed");
```

## Tokens

### tokens.list

`GET /v1/tokens` · [API reference](/docs/payments#list-tokens)

```ts
tokens.list(params?: { amount_usd?: string | number }): Promise<TokenInfo[]>
```

The coins your customers can pay with in the key's mode, with today's network
fee and minimum. With `amount_usd`, only the coins a payment of that amount
offers (its `token_options`), each with `amount_due_usd` when you pass the fees
on.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `amount_usd` | string or number | no | The order's amount, such as `"49.90"` |

**Returns** an array of [`TokenInfo`](#tokeninfo) (the library unwraps the
API's `{"tokens": […]}`).

**Errors** `PayitgoError` `400` (`amount_usd must be > 0`) for an amount that is
not a positive number, and the [common statuses](#payitgoerror).

```ts
const options = await payitgo.tokens.list({ amount_usd: "49.90" });
for (const t of options) {
  console.log(`${t.token} on ${t.chain}: pay $${t.amount_due_usd ?? "49.90"}, ${t.fee_note}`);
}
```

## Webhooks

The webhook (IPN) of the key's mode: one URL that receives every notification
of that mode, and the secret that signs them
([Webhooks](/docs/webhooks#the-webhook-api)). A test key manages the test
webhook, a live key the live one. A [storefront
key](/docs/authentication#storefront-keys) can `get` and `test` it, but not
`set` or `remove` it.

### webhooks.get

`GET /v1/webhook` · [API reference](/docs/webhooks#read-the-webhook)

```ts
webhooks.get(): Promise<Webhook>
```

**Returns** the [`Webhook`](#webhook): `url` (`null` when none is set) and
`secret`, the signing secret to give the client as `webhookSecret`.
It always succeeds for a valid key.

```ts
const { url, secret } = await payitgo.webhooks.get();
```

### webhooks.set

`PUT /v1/webhook` · [API reference](/docs/webhooks#set-the-url)

```ts
webhooks.set(url: string): Promise<Webhook>
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `url` | string | yes | An absolute `https://` URL on a public host that accepts `POST` |

**Returns** the [`Webhook`](#webhook) with its new `url`. The secret does not
change. Setting the URL it already has changes nothing.

**Errors** `PayitgoError` `400` (`url is required`, `url must use https`,
`url must be an absolute http(s) URL`, `url host does not resolve`,
`url must point at a public host`), `403` for a storefront key, `500`
(`could not set the webhook`), and the [common statuses](#payitgoerror).

```ts
await payitgo.webhooks.set("https://acme.example/payitgo/ipn");
```

### webhooks.remove

`DELETE /v1/webhook` · [API reference](/docs/webhooks#remove-the-url)

```ts
webhooks.remove(): Promise<Webhook>
```

Stops posting the mode's notifications to its URL. Payments created with their
own `webhook_url` still get theirs, and the secret is kept.

**Returns** the [`Webhook`](#webhook) with `url: null`.

**Errors** `403` for a storefront key, `500` (`could not remove the webhook`),
and the [common statuses](#payitgoerror).

### webhooks.test

`POST /v1/webhook/test` · [API reference](/docs/webhooks#send-a-test)

```ts
webhooks.test(): Promise<WebhookDelivery>
```

Queues a signed `ping` notification to the mode's URL; it arrives within about
5 seconds.

**Returns** the [`WebhookDelivery`](#webhookdelivery), with `status: "pending"`.

**Errors** `PayitgoError` `400` (`no webhook url is set for this mode: set one
first`), `500` (`could not queue the test`), and the
[common statuses](#payitgoerror).

```ts
const delivery = await payitgo.webhooks.test();
console.log(delivery.id, delivery.event_type, delivery.url); // 6710c3a1f2b4e5d6a7b8c9d0 ping https://…
```

### webhooks.constructEvent

No request · [how verification works](/docs/webhooks#verifying-the-signature)

```ts
webhooks.constructEvent<T = any>(
  payload: string | Uint8Array,
  signatureHeader: string | undefined | null,
  secret?: string,
  toleranceSeconds?: number, // default 300
): WebhookEvent<T>
```

Verifies a notification and returns its event. It computes the HMAC-SHA256 of
`t + "." + payload` with the secret, compares it with `v1` in constant time,
checks that `t` is within the tolerance of your clock, and only then parses the
body.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `payload` | string or `Uint8Array` (a `Buffer`) | yes | The **raw** request body, exactly as received. A body parsed and serialized again will not verify ([keep the raw body](/docs/webhooks#keep-the-raw-body)) |
| `signatureHeader` | string, `undefined` or `null` | yes | The `X-Webhook-Signature` header |
| `secret` | string | no | The signing secret, as payitgo gave it. Default: the client's `webhookSecret`. A secret passed here wins |
| `toleranceSeconds` | number | no | How far `t` may be from your clock, in seconds. Default `300`. `0` turns the timestamp check off (only for tests with captured bodies) |

**Returns** a [`WebhookEvent<T>`](#webhookevent). Pass a type to type `data`:
`constructEvent<Payment>(…)` for `payment.*` events.

**Errors** [`SignatureVerificationError`](#signatureverificationerror). Answer
`400`.

```ts
const event = payitgo.webhooks.constructEvent<Payment>(req.body, req.get("X-Webhook-Signature"));
if (event.type === "payment.confirmed") console.log(event.data.order_id);
```

### Payitgo.webhooks.constructEvent

```ts
Payitgo.webhooks.constructEvent<T = any>(
  payload: string | Uint8Array,
  signatureHeader: string | undefined | null,
  secret: string,
  toleranceSeconds?: number, // default 300
): WebhookEvent<T>
```

The same check without a client, for code that has the secret but no API key.
`secret` is required. Also exported as `webhooks`:

```ts
import { webhooks } from "@payitgo/node";

const event = webhooks.constructEvent(rawBody, signatureHeader, process.env.PAYITGO_WEBHOOK_SECRET!);
```

**Errors** [`SignatureVerificationError`](#signatureverificationerror), except
the "no webhook secret" one.

## Checkout

These two calls use the [public checkout API](/docs/checkout#the-public-checkout-api):
they are sent **without** your API key and are keyed by the payment's
`checkout_token`. They return the payer's view of a payment. On your
server you rarely need them: `payments.retrieve` and `payments.selectToken`
return more. The client still needs a valid key to be created.

### checkout.retrieve

`GET /checkout/{checkout_token}` · [API reference](/docs/checkout#read-the-payment)

```ts
checkout.retrieve(checkoutToken: string): Promise<Payment>
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `checkoutToken` | string | yes | The payment's `checkout_token` |

**Returns** a [`Payment`](#payment) without your private fields:
`merchant_id`, `metadata`, `customer_email`, `webhook_url`, `checkout_token`
and `checkout_url` are absent.

**Errors** `PayitgoError` `404` (`payment not found`), `0`, `429` and `5xx`.

### checkout.selectToken

`POST /checkout/{checkout_token}/token` · [API reference](/docs/checkout#choosing-the-coin)

```ts
checkout.selectToken(checkoutToken: string, token: string): Promise<Payment>
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `checkoutToken` | string | yes | The payment's `checkout_token` |
| `token` | string | yes | One of the payment's `token_options[].token` |

**Returns** the payer's view of the [`Payment`](#payment) with its coin,
address, amount and a 15-minute deadline.

**Errors** the same as [`payments.selectToken`](#paymentsselecttoken), and
`429` (`rate limit exceeded`) beyond 5 choices a second from one IP address.

```ts
const view = await payitgo.checkout.retrieve("b28d4f61e9a03c75d2e8f1a6");
if (view.token === "") {
  const chosen = await payitgo.checkout.selectToken("b28d4f61e9a03c75d2e8f1a6", "USDT_TRC20");
  console.log(chosen.deposit_address);
}
```

## Types

All types are exported from `payitgo`. Amounts are decimal strings (`"49.9"`) and
times are RFC 3339 strings (`"2026-10-10T09:15:00.112Z"`): parse amounts with a
decimal library and times with `new Date(…)`. Optional fields are absent from
the object when they have no value.

### Payment

A payment, as `payments.*`, `checkout.*` and `payment.*` events return it.
Every field is described in [the payment object](/docs/payments#the-payment-object).

```ts
interface Payment {
  id: string;
  merchant_id?: string;
  store_id?: string;
  order_id: string;
  status: PaymentStatus;
  livemode: boolean;
  simulation?: Simulation;     // a test payment's simulated outcome
  token: string;               // "" until the coin is chosen
  chain: string;               // "" until the coin is chosen
  token_options?: TokenInfo[]; // only while token is ""
  deposit_address: string;     // "" until the coin is chosen
  destination_tag?: number;    // XRP only
  crypto_amount: string;
  received_amount: string;
  usd_amount: string;
  price_amount?: string;       // your price in another currency, when you gave one
  price_currency?: string;     // its ISO 4217 code, upper case: "EUR"
  price_rate?: string;         // dollars per unit of price_currency (usd_amount: price_amount × price_rate, to the cent)
  amount_due_usd: string;
  fees_paid_by_customer: boolean;
  network_fee: string;
  network_fee_usd: string;
  settlement: "custodial" | "direct";
  received_usd: string;
  rate_locked: string;
  tx_id?: string;
  overpaid?: boolean;
  paid_late?: boolean;
  deposits: Deposit[];
  expires_at: string;
  created_at: string;
  confirmed_at?: string;
  network_fee_note: string;
  qr_payload: string;
  checkout_token?: string;
  checkout_url?: string;
  description?: string;
  metadata?: Record<string, string>;
  customer_email?: string;
  success_url?: string;
  cancel_url?: string;
  webhook_url?: string;
}
```

### PaymentStatus

```ts
type PaymentStatus =
  | "pending" | "partially_paid" | "detecting" | "confirmed"
  | "underpaid" | "expired" | "failed";
```

See the [lifecycle](/docs/payments#lifecycle). Only `confirmed` means you have
been paid.

### Simulation

A test payment's [simulated outcome](/docs/testing#simulate-a-payment).
Absent when it has none.

```ts
interface Simulation {
  outcome: SimulationOutcome;
  state: "requested" | "running" | "done"; // requested: asked for on create, waiting for the coin
}

type SimulationOutcome =
  | "confirmed" | "overpaid" | "partially_paid" | "underpaid"
  | "expired" | "failed" | "wrong_asset" | "paid_late";
```

### Deposit

One transfer to the payment's address ([the deposit object](/docs/payments#the-deposit-object)).

```ts
interface Deposit {
  tx_id: string;
  token: string;     // the asset that arrived
  chain: string;
  amount: string;
  usd_value: string;
  counted: boolean;  // counts toward the payment
  late?: boolean;    // arrived after the payment had closed
  status: "pending" | "confirmed" | "failed";
  seen_at: string;
  confirmed_at?: string;
  from?: string;        // the sender, when the network reports it: where a refund would go
  block?: number;       // the block (XRP: the ledger) that holds it
  made_at?: string;     // that block's time
  failed_at?: string;   // when it failed
  fail_reason?: string; // "transaction not found on chain" or "confirmation timeout"
  simulated?: boolean;  // played by a test payment's simulation: its tx_id is on no chain
}
```

### CreatePaymentParams

The parameters of [`payments.create`](#paymentscreate).

```ts
interface CreatePaymentParams {
  order_id: string;
  amount_usd?: number | string;     // the price in dollars, or:
  price_amount?: number | string;   // the price in another currency…
  price_currency?: string;          // …and its ISO 4217 code ("EUR")
  token?: string;
  tokens?: string[];
  description?: string;
  metadata?: Record<string, string>;
  customer_email?: string;
  success_url?: string;
  cancel_url?: string;
  webhook_url?: string;
  simulate?: SimulationOutcome; // test keys only
}
```

### ListPaymentsParams

The parameters of [`payments.list`](#paymentslist).

```ts
interface ListPaymentsParams {
  status?: PaymentStatus;
  chain?: string;
  token?: string;
  store_id?: string;
  limit?: number;
  offset?: number;
}
```

### PaymentList

```ts
interface PaymentList {
  payments: Payment[];
  total: number;  // matches across all pages
  limit: number;  // the page size applied
  offset: number; // the offset applied
}
```

### TokenInfo

A coin the customer can pay with ([the TokenInfo object](/docs/payments#the-tokeninfo-object)).

```ts
interface TokenInfo {
  token: string;            // "USDT_TRC20"
  chain: string;            // "tron"
  min_usd: string;          // the smallest amount_usd accepted right now
  network_fee_usd: string;  // the network fee right now
  amount_due_usd?: string;  // with an amount, when you pass the fees on
  fee_note: string;
}
```

### Webhook

The webhook of the key's mode ([the webhook object](/docs/webhooks#the-webhook-object)).

```ts
interface Webhook {
  livemode: boolean;
  url: string | null; // null when none is set
  secret: string;     // signs the mode's notifications
  created_at: string;
  updated_at: string;
}
```

### WebhookDelivery

One notification on its way to one URL ([the delivery object](/docs/webhooks#the-delivery-object)).

```ts
interface WebhookDelivery {
  id: string;                      // the X-Webhook-Id header
  merchant_id: string;
  target: "account" | "payment";   // the mode's URL, or the payment's own webhook_url
  url: string;
  event_id: string;                // the event's id
  event_type: string;
  payload: string;                 // the exact body posted
  status: "pending" | "sending" | "delivered" | "failed" | "resolved";
  attempts: number;
  max_attempts: number;            // 7
  next_attempt_at: string;
  last_status_code?: number;
  last_error?: string;
  last_attempt_at?: string;
  delivered_at?: string;
  resolved_at?: string;
  resolution_note?: string;
  created_at: string;
  history?: WebhookDeliveryAttempt[];
}

interface WebhookDeliveryAttempt {
  at: string;
  url?: string;
  signed?: string;      // the t of the attempt's signature
  status_code?: number;
  error?: string;
  response?: string;    // the start of your server's answer
  millis?: number;
  note?: string;        // a redelivery or resolution, and by whom
}
```

### WebhookEvent

A verified notification ([the body](/docs/webhooks#body)).

```ts
interface WebhookEvent<T = any> {
  id: string;         // the same on every retry: deduplicate on it
  type: string;       // one of WebhookEventType
  livemode: boolean;
  created_at: string;
  data: T;            // the payment, settlement, payout, or {message: "pong"}
}

type WebhookEventType =
  | "payment.partially_paid" | "payment.detecting" | "payment.confirmed"
  | "payment.underpaid" | "payment.expired" | "payment.failed"
  | "payment.wrong_asset" | "payment.late_deposit"
  | "payout.approved" | "payout.rejected" | "payout.sent"
  | "settlement.completed" | "settlement.failed"
  | "ping";
```

`data` is a [`Payment`](#payment) for `payment.*` events, a
[settlement](/docs/settlement#following-settlements) for `settlement.*`, a
[payout](/docs/account#the-payout-object) for `payout.*`, and
`{ message: "pong" }` for `ping`. See the [events](/docs/webhooks#events).

### PayitgoOptions

The [constructor's options](#new-payitgo).

```ts
interface PayitgoOptions {
  baseUrl?: string;
  timeoutMs?: number;
  maxRetries?: number;
  webhookSecret?: string;
  webhookUrl?: string;
}
```

## Examples

### Your own payment page

Show the coins on your own site, create the payment in the coin the customer
picks, and render the amount, address, QR code and deadline from the payment
itself ([what to show](/docs/checkout#what-to-show)). Express, with the
[`qrcode`](https://www.npmjs.com/package/qrcode) package for the QR code:

```ts
// npm install @payitgo/node express qrcode
import express from "express";
import QRCode from "qrcode";
import { Payitgo, PayitgoError, type Payment } from "@payitgo/node";

const payitgo = new Payitgo(process.env.PAYITGO_KEY!, { baseUrl: process.env.PAYITGO_BASE_URL });
const app = express();
app.use(express.urlencoded({ extended: false }));

const NETWORKS: Record<string, string> = {
  tron: "Tron (TRC-20)", ethereum: "Ethereum (ERC-20)", bsc: "BNB Smart Chain (BEP-20)",
  bitcoin: "Bitcoin", litecoin: "Litecoin", dogecoin: "Dogecoin", xrp: "XRP Ledger",
};
const esc = (s: unknown) =>
  String(s).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);

// 1. The coins this order can be paid with, and what each costs the customer.
app.get("/orders/:id/pay", async (req, res) => {
  const order = await findOrder(req.params.id); // your code: { id, totalUsd: "49.90" }
  const tokens = await payitgo.tokens.list({ amount_usd: order.totalUsd });
  res.send(`<form method="post">${tokens
    .map((t) => `<button name="token" value="${esc(t.token)}">
        ${esc(t.token)} on ${esc(NETWORKS[t.chain] ?? t.chain)}: $${esc(t.amount_due_usd ?? order.totalUsd)}
      </button>`)
    .join("")}</form>`);
});

// 2. The customer picked one: create the payment in it and show it.
app.post("/orders/:id/pay", async (req, res) => {
  const order = await findOrder(req.params.id);
  try {
    const payment = await payitgo.payments.create({
      order_id: order.id,          // the same order again returns its open payment
      amount_usd: order.totalUsd,
      token: String(req.body.token),
    });
    res.send(await renderPayment(payment));
  } catch (err) {
    // Log the API's message (err.status, err.message); do not show it to the customer.
    console.error(err instanceof PayitgoError ? `payitgo ${err.status}: ${err.message}` : err);
    res.status(502).send("We could not start this payment. Please try again.");
  }
});

async function renderPayment(p: Payment): Promise<string> {
  const qr = await QRCode.toDataURL(p.qr_payload);
  return `
    ${p.livemode ? "" : "<p><b>Test payment:</b> do not send coins to this address.</p>"}
    <p>Send exactly <b>${esc(p.crypto_amount)} ${esc(p.token.split("_")[0])}</b>
       on <b>${esc(NETWORKS[p.chain] ?? p.chain)}</b> to</p>
    <p><code>${esc(p.deposit_address)}</code></p>
    ${p.destination_tag !== undefined ? `<p>Destination tag (required): <b>${p.destination_tag}</b></p>` : ""}
    <img src="${qr}" width="220" height="220" alt="QR code">
    <p>Pay before <time datetime="${esc(p.expires_at)}">${esc(new Date(p.expires_at).toUTCString())}</time></p>`;
}
```

A payment keeps its coin while it is open: if the customer goes back and picks
another coin, `payments.create` for the same order returns the first payment
unchanged. To let the customer choose on your page **after** the payment
exists, create it without `token` and set the coin with `selectToken`:

```ts
const payment = await payitgo.payments.create({ order_id: order.id, amount_usd: order.totalUsd });
// render payment.token_options (each with token, chain, min_usd, amount_due_usd, fee_note)…
// …and when the customer picks one:
const chosen = await payitgo.payments.selectToken(payment.id, pickedToken);
res.send(await renderPayment(chosen));
```

To follow the payment live on the page, connect the page's script to the
socket's default namespace (`/`) and `subscribe` with `payment.checkout_token`
(with an acknowledgement): every `payment:update` then carries the whole
payment ([follow it live](/docs/checkout#follow-it-live)). No polling.
Fulfil the order from the webhook, never from the page.

### A webhook (IPN) handler with Express

Read the raw body, verify it, handle each event once, and answer `2xx` within
10 seconds ([answering and retries](/docs/webhooks#answering-and-retries)):

```ts
import express from "express";
import { Payitgo, type Payment } from "@payitgo/node";

const payitgo = new Payitgo(process.env.PAYITGO_KEY!, {
  baseUrl: process.env.PAYITGO_BASE_URL,
  webhookSecret: process.env.PAYITGO_WEBHOOK_SECRET, // the signing secret of the key's mode
});
const app = express();

// express.raw keeps the body as a Buffer. Register this route before any
// app.use(express.json()), or the body is parsed and will not verify.
app.post("/payitgo/ipn", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = payitgo.webhooks.constructEvent<Payment>(req.body, req.get("X-Webhook-Signature"));
  } catch {
    return res.sendStatus(400); // not from payitgo, changed, or too old
  }

  if (await isProcessed(event.id)) return res.sendStatus(200); // a retry or redelivery

  if (event.livemode === (await payitgo.isLive())) { // ignore the other mode's events
    switch (event.type) {
      case "payment.confirmed":                // final: fulfil
        await markOrderPaid(event.data.order_id, event.data.id); // idempotent
        break;
      case "payment.expired":                  // nothing arrived in time
        await releaseOrder(event.data.order_id);
        break;
    }
  }

  await markProcessed(event.id);
  res.sendStatus(200);
});

app.listen(3000);
```

`isProcessed`, `markProcessed`, `markOrderPaid` and `releaseOrder` are your own
code: store processed event ids in a table with a unique key, and make
fulfilment safe to run twice. If your code throws, Express answers `500` and
payitgo retries the notification. More in
[handle events safely](/docs/webhooks#handle-events-safely).

### Test mode

Build everything with your test key: the same calls, simulated payments
instead of coins, and the test webhook with its own secret
([Test mode](/docs/testing)).

```ts
const payitgo = new Payitgo(process.env.PAYITGO_TEST_KEY!, { baseUrl: "https://api.payitgo.co" }); // your test key
console.log(await payitgo.isLive()); // false

// Point the TEST webhook at your development server and read its secret.
const hook = await payitgo.webhooks.set("https://dev.acme.example/payitgo/ipn");
console.log(hook.secret);    // test mode's signing secret: your dev PAYITGO_WEBHOOK_SECRET

// A signed ping arrives within seconds.
await payitgo.webhooks.test();

// A test payment that pays itself: payment.detecting, then payment.confirmed,
// arrive at the test webhook within half a minute.
const payment = await payitgo.payments.create({
  order_id: "TEST-1", amount_usd: 10, token: "USDT_TRC20", simulate: "confirmed",
});
console.log(payment.livemode, payment.simulation?.state); // false "running"

// Another outcome on a payment you already have:
await payitgo.payments.simulate("TEST-2", "underpaid");
```

Never send real coins to a test address: nothing watches it
([why](/docs/testing#never-send-real-coins-to-a-test-address)).

Check your verification against the example in
[the recipe](/docs/webhooks#the-recipe) (tolerance `0`, because the example is
old):

```ts
const body = '{"created_at":"2026-10-10T09:12:44.512837Z","data":{"message":"pong"},"id":"5c0e1f9a2b7d4e3c8a6f1b2d","livemode":false,"type":"ping"}';
const event = Payitgo.webhooks.constructEvent(
  body,
  "t=1791623565,v1=1af0ba31872e3152c422edaf9442c9f0eb4ebd202beebafbaaba14f77336194b",
  "a3858862701d1ce11ab06cb5dd9bb941d4849283f81fc0c3f0db0d99a2b82d48",
  0,
);
console.log(event.type); // "ping"
```

To test your handler without the network, sign a body yourself:

```ts
import { createHmac } from "node:crypto";

function sign(body: string, secret: string, t = Math.floor(Date.now() / 1000)): string {
  return `t=${t},v1=${createHmac("sha256", secret).update(`${t}.${body}`).digest("hex")}`;
}
// POST body to your handler with the header X-Webhook-Signature: sign(body, secret)
```

Going live: put the live key and the **live** webhook secret in your
production environment, set the live webhook URL with the live key, and keep
the `event.livemode` check in your handler ([going live](/docs/testing#going-live)).
