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