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, Webhooks and Checkout pages describe every field this library returns.

Method HTTP Returns
payments.create(params) POST /v1/payments Payment
payments.retrieve(id) GET /v1/payments/{id} Payment
payments.list(params?) GET /v1/payments PaymentList
payments.selectToken(id, token) POST /v1/payments/{id}/token Payment
payments.simulate(id, outcome) POST /v1/payments/{id}/simulate Payment
tokens.list(params?) GET /v1/tokens TokenInfo[]
webhooks.get() GET /v1/webhook Webhook
webhooks.set(url) PUT /v1/webhook Webhook
webhooks.remove() DELETE /v1/webhook Webhook
webhooks.test() POST /v1/webhook/test WebhookDelivery
webhooks.constructEvent(body, header, secret?, tolerance?) none (local) WebhookEvent
Payitgo.webhooks.constructEvent(body, header, secret, tolerance?) none (local, no client) WebhookEvent
checkout.retrieve(checkoutToken) GET /checkout/{checkout_token} Payment
checkout.selectToken(checkoutToken, token) POST /checkout/{checkout_token}/token Payment
isLive() GET /v1/webhook, the first time only boolean

Every method that calls the API returns a Promise.

Install

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)
// 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

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 and fulfil the order when payment.confirmed arrives.

Configuration

new Payitgo

new Payitgo(apiKey: string, options?: PayitgoOptions)
Parameter Type Required Default Description
apiKey string yes Your secret key, test or live (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
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 uses it when you pass none
options.webhookUrl string no none Sent as webhook_url with every payments.create 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
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
tokens object Tokens
webhooks object Webhooks
checkout object Checkout
Payitgo.webhooks object (static) constructEvent 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

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; 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: 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.

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:

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 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() asks payitgo. Pair each key with the webhook secret of the same mode. See 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.

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).

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 Yes
500 and other 5xx Something failed on payitgo's side Yes
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 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)
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

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) 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). 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). 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). Default: the client's webhookUrl. Pass "" to send none for this payment, even with a client default
simulate SimulationOutcome no Test keys only: the outcome to simulate. With token it starts at once; without, once the coin is chosen

Returns the Payment: the new one (201) or the order's open one (200); both resolve the same way. Fields: 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. 503 when no exchange rate can be had for price_currency (after the retries): try again shortly. Also 401, 403, 429, 500 and 0 as for every call.

// 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

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.

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.

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

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

Payments of the key's mode, newest first.

Parameter Type Required Default Description
status PaymentStatus no all Only payments in this status
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 (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: payments, total (matches across all pages), and the limit and offset applied.

Errors the common statuses only. An unknown status, chain or token matches nothing.

// 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

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. 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, 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)
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

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). Test keys only.

Parameter Type Required Description
id string yes The payment's id or your order_id
outcome SimulationOutcome yes Such as "confirmed" (what each does)

Returns the whole 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"
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

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 (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.

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). A test key manages the test webhook, a live key the live one. A storefront key can get and test it, but not set or remove it.

webhooks.get

GET /v1/webhook · API reference

webhooks.get(): Promise<Webhook>

Returns the 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.

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

webhooks.set

PUT /v1/webhook · API reference

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 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.

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

webhooks.remove

DELETE /v1/webhook · API reference

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 with url: null.

Errors 403 for a storefront key, 500 (could not remove the webhook), and the common statuses.

webhooks.test

POST /v1/webhook/test · API reference

webhooks.test(): Promise<WebhookDelivery>

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

Returns the 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.

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

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)
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>. Pass a type to type data: constructEvent<Payment>(…) for payment.* events.

Errors SignatureVerificationError. Answer 400.

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

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:

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

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

Errors SignatureVerificationError, except the "no webhook secret" one.

Checkout

These two calls use 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

checkout.retrieve(checkoutToken: string): Promise<Payment>
Parameter Type Required Description
checkoutToken string yes The payment's checkout_token

Returns a 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

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 with its coin, address, amount and a 15-minute deadline.

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

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.

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

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

See the lifecycle. Only confirmed means you have been paid.

Simulation

A test payment's simulated outcome. Absent when it has none.

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).

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.

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.

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

PaymentList

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).

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).

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).

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).

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 for payment.* events, a settlement for settlement.*, a payout for payout.*, and { message: "pong" } for ping. See the events.

PayitgoOptions

The constructor's options.

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). Express, with the qrcode package for the QR code:

// 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:

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). 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):

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.

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).

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).

Check your verification against the example in the recipe (tolerance 0, because the example is old):

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:

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).