Account and team
Everything about your account (team, stores, settings, the webhook's signing
secret, balances and payouts) is managed through the dashboard API under
/merchant, signed in as a person. Your servers only ever need an API key
and the /v1 routes.
Every route on this page except signup, login and refresh takes an access token from sign-in:
Authorization: Bearer <access_token>
Routes that read test or live data take ?mode=test for test data; without
it they return live data.
| Section | Routes |
|---|---|
| Signing up and in | signup, login, refresh, logout, me, password |
| Team | /merchant/users |
| Stores | /merchant/stores |
| Payment settings | /merchant/settings, /merchant/tokens |
| Live mode | /merchant/live |
| Webhook | /merchant/webhook, /merchant/webhook/roll-secret, /merchant/webhooks/deliveries |
| Payments and dashboard | /merchant/dashboard, /merchant/payments, events, trail |
| Audit log | /merchant/audit |
| Balances and payouts | balances, ledger, payout addresses, payouts |
| Support chat | /merchant/support |
| Referral program | /merchant/affiliate, /merchant/affiliate/payouts, /referrals/{code}/click |
| Real-time events | socket.io /merchant |
API keys are on the Authentication page.
Roles
| Role | Can |
|---|---|
owner |
Everything: team, settings, live mode, the audit log, rolling the webhook secret, payouts, the referral program |
developer |
API keys, stores, the webhook URL and test, redelivering webhooks |
finance |
Payout addresses and payouts, the referral program |
viewer |
Read payments, the dashboard, balances, the ledger, settlements and webhook deliveries; change nothing |
Every role can read payments, the dashboard, balances, the ledger,
settlements, payouts and webhook deliveries, and write in the
support chat. A route your role may not use answers
403 {"error": "insufficient permissions"}.
Signing up and in
Open an account
POST /merchant/signup · no authentication · 5 a second per IP address (a limit of its own: sign-in and checkout have their own)
| Field | Type | Required | Description |
|---|---|---|---|
merchant_name |
string | yes | Your business's name |
email |
string | yes | The owner's email address, used to sign in |
password |
string | yes | At least 8 characters |
referral_code |
string | no | The code of the referral link that brought you (its ?ref=), in any case. An unknown or suspended code is ignored: it never stops the signup (Referral program) |
curl -s https://api.payitgo.co/merchant/signup \
-H 'Content-Type: application/json' \
-d '{"merchant_name": "Acme Store", "email": "you@acme.example", "password": "a-long-password"}'
201 Created. It holds your first two API keys, shown only here, and
signs you in (an access token, and the refresh cookie):
{
"merchant": {
"id": "3f8a1c27b94e05d6a2c71e90",
"name": "Acme Store",
"status": "active",
"fee_bps": 100,
"live_enabled": false,
"underpaid_tolerance_bps": 50,
"accept_other_assets": false,
"settlement": "direct",
"fees_paid_by_customer": true,
"convert_into": "cheapest",
"hide_below_minimum": false,
"enabled_tokens": ["USDT_TRC20", "TRX", "ETH", "USDT_ERC20", "USDC_ERC20", "BNB", "USDT_BEP20", "USDC_BEP20", "BTC", "LTC", "DOGE", "XRP"],
"created_at": "2026-10-10T08:57:25.902774252Z",
"updated_at": "2026-10-10T08:57:25.902774252Z"
},
"user": {
"id": "9b2e4c71d8a05f3e6c1a7b24",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"email": "you@acme.example",
"role": "owner",
"created_at": "2026-10-10T08:57:25.902774252Z"
},
"api_keys": [
{
"api_key": { "id": "4f2a9c1e7b3d5a08c6e1d7b2", "merchant_id": "3f8a1c27b94e05d6a2c71e90", "livemode": false, "label": "default", "last4": "5a8f", "created_by": "9b2e4c71d8a05f3e6c1a7b24", "created_at": "2026-10-10T08:57:25.904339745Z" },
"secret": "4f9c2a7e1b8d3f6a0c5e9b2d7a4f1c8e3b6d9a2f5c8e1b4d7a0f3c6e9b2d5a8f"
},
{
"api_key": { "id": "8d3b6e2f1a4c9e07b5d2a8c4", "merchant_id": "3f8a1c27b94e05d6a2c71e90", "livemode": true, "label": "default", "last4": "e0f5", "created_by": "9b2e4c71d8a05f3e6c1a7b24", "created_at": "2026-10-10T08:57:25.905901832Z" },
"secret": "ea2bc4d2247c7159eec631a51a086a4a6bbd156cedb9982ea99510c5a9f3e0f5"
}
],
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJraW5k…",
"expires_at": "2026-10-10T09:12:25.907004586Z",
"role": "owner"
}
| Status | Message | Cause |
|---|---|---|
400 |
merchant_name is required · invalid email address · password must be at least 8 characters |
A field is missing or invalid |
409 |
email already registered |
That email already has an account |
429 |
rate limit exceeded |
Too many attempts from your IP address |
The merchant object
| Field | Type | Description |
|---|---|---|
id |
string | Your account id |
name |
string | Your business's name |
status |
string | active, or suspended by payitgo (no API calls, no sign-ins, no payments) |
fee_bps |
integer | payitgo's service fee in basis points (100 = 1%), agreed with your account |
live_enabled |
boolean | Whether live mode is on (Live mode) |
underpaid_tolerance_bps |
integer | How far short a payment may fall and still count as paid, in basis points |
accept_other_assets |
boolean | Whether a deposit in another accepted coin counts toward a payment |
settlement |
string | direct, custodial or scheduled |
payout_schedule |
string | Scheduled settlement: instant, daily or weekly (Mondays) |
payout_scheduled_at |
string, optional | Scheduled settlement: when your balance was last paid out |
fees_paid_by_customer |
boolean | Whether customers pay the fees on top of your price (on for a new account) |
convert_into |
string | Direct settlement: what a coin you have no payout address for is converted into: cheapest (the default), order, or your preferred payout coin (which coin) |
hide_below_minimum |
boolean | Whether a coin converted across networks is left out of a payment too small for the exchange's minimum, rather than the customer paying up to it (minimums) |
enabled_tokens |
array of strings | The coins you accept |
created_at, updated_at |
string | When the account was opened and last changed |
Payment settings explains each setting.
The user object
| Field | Type | Description |
|---|---|---|
id |
string | The team member's id |
merchant_id |
string | Your account |
email |
string | Their sign-in email |
role |
string | owner, developer, finance or viewer |
created_at |
string | When they were added |
last_login_at |
string, optional | Their last sign-in |
Sign in, refresh, sign out
POST /merchant/login, POST /merchant/auth/refresh,
POST /merchant/auth/logout and POST /merchant/auth/logout-all are described,
with their responses and errors, on the
Authentication page.
Who am I
GET /merchant/me · any role
curl -s https://api.payitgo.co/merchant/me -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK: {"merchant": {…}, "user": {…}}, the merchant
with its current settings and the signed-in user.
Change your password
POST /merchant/me/password · any role
| Field | Type | Required | Description |
|---|---|---|---|
current_password |
string | yes | Your password now |
new_password |
string | yes | At least 8 characters |
curl -s https://api.payitgo.co/merchant/me/password \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"current_password": "a-long-password", "new_password": "an-even-longer-password"}'
200 OK: {"message": "password changed; sign in again"}. Every session of
yours ends.
| Status | Message | Cause |
|---|---|---|
400 |
current_password and new_password are required |
A field is missing |
400 |
password must be at least 8 characters |
new_password is too short |
403 |
current password is incorrect |
Wrong current_password |
Team
Invite colleagues with the role they need. Each person signs in with their own email and password. All four routes are owner only.
| Method | Path | Body | Response |
|---|---|---|---|
GET |
/merchant/users |
200 {"users": [user, …]} |
|
POST |
/merchant/users |
email, password, role |
201 {"user": user} |
PATCH |
/merchant/users/{id} |
role |
200 {"user": user} |
DELETE |
/merchant/users/{id} |
200 {"message": "user removed"} |
curl -s https://api.payitgo.co/merchant/users \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"email": "dev@acme.example", "password": "a-first-password", "role": "developer"}'
201 Created:
{
"user": {
"id": "5e8a2c9d1f4b7036a2e9c1d8",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"email": "dev@acme.example",
"role": "developer",
"created_at": "2026-10-10T08:59:43.427614842Z"
}
}
The owner sets a first password and hands it over; the new member changes it
with /merchant/me/password. Changing a role or removing someone ends their
sessions (an access token already issued stays valid for up to 15 minutes).
| Status | Message | Cause |
|---|---|---|
400 |
invalid email address · password must be at least 8 characters |
Invalid new member |
400 |
role must be one of owner, developer, finance, viewer |
Unknown role |
404 |
not found |
No such member in your account |
409 |
email already registered |
That email already belongs to someone |
409 |
the account must keep at least one owner |
Demoting or removing the last owner |
Stores
Selling from more than one shop (several websites, an app and a website, a few
branches)? Create a store for each and give each its own API keys. Every
payment a store's key creates carries the store's store_id, and the store's
backend can send its notifications to its own URL with
webhook_url. A merchant with one
shop needs no stores.
| Method | Path | Role | Body | Response |
|---|---|---|---|---|
GET |
/merchant/stores |
any | 200 {"stores": [store, …]} |
|
POST |
/merchant/stores |
owner, developer | name (1 to 100 characters) |
201 {"store": store} |
PATCH |
/merchant/stores/{id} |
owner, developer | name |
200 {"store": store} |
curl -s https://api.payitgo.co/merchant/stores \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name": "Acme EU shop"}'
201 Created:
{
"store": {
"id": "6d2a9e4c1b7f30a58e2c4d91",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"name": "Acme EU shop",
"created_at": "2026-10-10T08:59:52.920128021Z",
"updated_at": "2026-10-10T08:59:52.920128021Z"
}
}
Then give it a key: POST /merchant/api-keys?mode=test {"label": "eu-shop", "store_id": "6d2a9e4c1b7f30a58e2c4d91"}
(Create a key). Errors:
400 name is required (up to 100 characters), 404 not found.
Each store has its own order ids. An order_id is unique within a store:
a store's key creates and finds its own store's orders only, and a key with
no store those made with no store's key. Two shops on one account can both
number their orders from 1: give each the key of its own store, and their
order_ids never meet (safe retries).
Payment settings
PATCH /merchant/settings · owner
Send only the settings you change. A payment keeps the settings in force when it was created.
| Field | Type | Default | Description |
|---|---|---|---|
underpaid_tolerance_bps |
integer | 50 (0.5%) |
How far short a payment may fall and still count as paid, 0 to 2000 (20%) (wrong amount) |
accept_other_assets |
boolean | false |
Whether a deposit in another accepted coin counts toward the payment at its dollar value (wrong asset) |
settlement |
string | direct |
direct (your payout address for the coin, or converted into one of your payout coins when you have none ready for it; your balance when it cannot be), custodial (your balance, in USDT on Tron) or scheduled (your balance, paid out to you on a schedule) (where the money goes) |
payout_schedule |
string | daily |
Scheduled settlement: instant (a payout as soon as each payment is credited), daily, or weekly (on Mondays) |
fees_paid_by_customer |
boolean | true |
Whether the customer pays the service and network fees on top of your price; off, they come out of what you receive (passing fees on) |
convert_into |
string | cheapest |
Direct settlement: what a coin you have no payout address for is converted into. cheapest: whichever of your payout coins costs the least at each payment's amount; order: the first of a fixed order; a coin (BTC, USDT_BEP20…, any coin you have a payout address for): your preferred payout coin, used whenever a payment can be converted into it (which coin) |
hide_below_minimum |
boolean | false |
Direct settlement, with the fees passed on: false offers a coin converted across networks even for a payment too small for the exchange's minimum, the customer paying up to it (you receive the extra, extra_usd); true offers it only from the price that reaches the minimum (minimums). When you pay the fees yourself, such a coin is always offered only from that price |
enabled_tokens |
array of strings | every token | The coins you accept, at least one (accepted tokens) |
curl -s -X PATCH https://api.payitgo.co/merchant/settings \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"underpaid_tolerance_bps": 100, "enabled_tokens": ["USDT_TRC20", "USDT_BEP20", "USDC_BEP20", "TRX"]}'
200 OK: {"merchant": {…}}, the merchant object
with the new settings.
| Status | Message | Cause |
|---|---|---|
400 |
nothing to update |
An empty body |
400 |
underpaid_tolerance_bps must be between 0 and 2000 |
Out of range |
400 |
settlement must be "custodial", "direct" or "scheduled" |
Unknown mode |
400 |
unsupported token: SHIB |
An unknown entry in enabled_tokens |
400 |
enabled_tokens must enable at least one token |
An empty list |
403 |
insufficient permissions |
Not the owner |
Accepted tokens
enabled_tokens is the list of coins you accept, such as
["USDT_TRC20", "USDT_BEP20", "USDC_BEP20"]. Only those are offered to your
customers, listed by GET /v1/tokens, and allowed in a payment's token or
tokens (others get 400 not enabled for your account: …). A deposit in a
coin you have not enabled still reaches you, but never counts toward the
payment. Enabling every token also accepts tokens payitgo adds later.
GET /merchant/tokens?mode=test · any role
Every supported token, marked enabled or not, with where its payments go and
its fee and minimum right now: what a settings screen shows. Without
mode=test, the live ones.
curl -s 'https://api.payitgo.co/merchant/tokens' -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK (two of the twelve tokens shown; direct settlement, with a live
payout address for USDT on Tron only, so BTC is converted into it):
{
"tokens": [
{ "token": "USDT_TRC20", "chain": "tron", "min_usd": "6.44", "network_fee_usd": "3.22", "fee_note": "Final in about a minute.", "enabled": true, "settles_to": "wallet" },
{ "token": "BTC", "chain": "bitcoin", "min_usd": "13.2", "network_fee_usd": "6.6", "network_fee_bps": 180, "fee_note": "Final in about 30 minutes (3 confirmations).", "enabled": false, "settles_to": "wallet", "paid_out_in": "USDT_TRC20" }
]
}
Each entry is a TokenInfo with three more fields:
| Field | Type | Description |
|---|---|---|
enabled |
boolean | Whether you accept it |
settles_to |
string | wallet (direct settlement, with a payout address for it, in use or still in its 24 hours, or one in use for the coin it is converted into) or balance (custody or scheduled settlement, or a coin that converts into none of your payout coins, such as any coin without a test payout address in test mode) |
paid_out_in |
string, optional | The payout coin a payment in it is converted into on its way to your wallet, when you have no payout address for it (coins you have no payout address for). Absent when it is paid out as it is (an address still in its 24 hours included: its payments wait for it), or kept in your balance |
Live mode
POST /merchant/live · owner
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | yes | true turns live mode on, false off |
curl -s https://api.payitgo.co/merchant/live \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"enabled": true}'
200 OK: {"merchant": {…, "live_enabled": true, …}}. From now on the live
key works. Turning it off stops new live payments; open ones still complete.
400 {"error": "enabled (true or false) is required"} when enabled is
missing.
Webhook
Each mode has one webhook: a URL and a signing secret
(Webhooks). Your server manages the URL with its API key
(/v1/webhook); people do it here, choosing the mode with ?mode=test
(default live). Rolling the secret is only possible here.
| Method | Path | Role | Response |
|---|---|---|---|
GET |
/merchant/webhook?mode=test |
owner, developer | 200 {"webhook": Webhook} |
PUT |
/merchant/webhook?mode=test |
owner, developer | 200 {"webhook": Webhook} |
DELETE |
/merchant/webhook?mode=test |
owner, developer | 200 {"webhook": Webhook} with "url": null |
POST |
/merchant/webhook/test?mode=test |
owner, developer | 202 {"delivery": Delivery} |
POST |
/merchant/webhook/roll-secret?mode=test |
owner | 200 {"webhook": Webhook} with a new secret |
GET |
/merchant/webhooks/deliveries?mode=test |
any | 200 {"deliveries": [Delivery, …]} |
GET |
/merchant/webhooks/deliveries/{id} |
any | 200 {"delivery": Delivery} |
POST |
/merchant/webhooks/deliveries/{id}/redeliver |
owner, developer | 202 {"message": "redelivery queued"} |
The first five behave exactly like their /v1/webhook
counterparts, with the same body ({"url": "https://…"} for PUT) and the
same errors. The objects are described there:
Webhook and
Delivery.
Roll the signing secret
POST /merchant/webhook/roll-secret?mode=test · owner
Replaces the mode's signing secret. Every notification sent from now on, retries of older deliveries included, is signed with the new secret. The old one stops working at once, so have your server ready to accept the new one (for example by trying both for a few minutes) before you roll it.
curl -s -X POST 'https://api.payitgo.co/merchant/webhook/roll-secret?mode=test' \
-H "Authorization: Bearer $ACCESS_TOKEN"
200 OK:
{
"webhook": {
"livemode": false,
"url": "https://acme.example/payitgo/ipn",
"secret": "b81c…07fa",
"created_at": "2026-10-10T08:58:44.128Z",
"updated_at": "2026-10-10T08:59:29.906Z"
}
}
A developer gets 403 {"error": "insufficient permissions"}.
Delivery history
GET /merchant/webhooks/deliveries · any role
Every notification payitgo sent you (or tried to), newest first, with every attempt.
| Query parameter | Default | Description |
|---|---|---|
mode |
live | test for test deliveries |
status |
all | pending, sending, delivered, failed or resolved |
limit |
50 | 1 to 200 |
curl -s 'https://api.payitgo.co/merchant/webhooks/deliveries?mode=test&status=failed&limit=20' \
-H "Authorization: Bearer $ACCESS_TOKEN"
200 OK: {"deliveries": [Delivery, …]} (no paging beyond limit). One
delivery, with its full history: GET /merchant/webhooks/deliveries/{id},
200 {"delivery": Delivery} or 404 {"error": "not found"}. An example is
under Delivery history and redelivery.
Redeliver
POST /merchant/webhooks/deliveries/{id}/redeliver · owner, developer
Sends a delivery again from the start (attempts back to 0, seven more tries),
whatever its status: after you fixed your handler, or to replay an event.
It is posted to the account's webhook URL as it is now (or, for a payment's
own webhook_url, to that URL), signed with the current secret.
curl -s -X POST https://api.payitgo.co/merchant/webhooks/deliveries/8e3c1a7f5d9b2e4c6a0f1d3b/redeliver \
-H "Authorization: Bearer $ACCESS_TOKEN"
202 Accepted:
{"message": "redelivery queued"}
An unknown id answers 404 {"error": "not found"}.
Payments and dashboard
| Method | Path | Role | Response |
|---|---|---|---|
GET |
/merchant/dashboard?mode=test |
any | Today's figures, and confirmed payments over 7 days, 30 days and all time |
GET |
/merchant/payments?mode=test |
any | Payments, as GET /v1/payments |
GET |
/merchant/payments/{id} |
any | One payment, by payment id, test or live |
GET |
/merchant/payments/{id}/events |
any | The payment's event timeline |
GET |
/merchant/payments/{id}/trail |
any | Everything about one payment, for a dispute |
POST |
/merchant/payments/{id}/simulate |
owner, developer, finance | Plays an outcome on a test payment (below) |
/merchant/payments takes the same filters as GET /v1/payments (status,
token, chain, store_id, limit, offset) plus mode. An unknown
payment answers 404 {"error": "payment not found"}.
Every list on this page answers the limit and offset it applied. A limit
outside its range (1 to 200; 1 to 500 for the ledger and the audit log) is
replaced by the default (50; 100 for the audit log and a payment's events),
and a negative offset by 0.
Dashboard
curl -s 'https://api.payitgo.co/merchant/dashboard?mode=test' -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK:
{
"livemode": false,
"confirmed_today": 0,
"volume_usd_today": "0.00",
"pending": 4,
"expired_today": 0,
"failed_webhooks": 0,
"by_chain": {},
"periods": {
"today": {"confirmed": 0, "volume_usd": "0.00", "by_chain": {}},
"last_7_days": {"confirmed": 2, "volume_usd": "15.50", "by_chain": {"tron": "10.00", "bsc": "5.50"}},
"last_30_days": {"confirmed": 3, "volume_usd": "115.50", "by_chain": {"tron": "110.00", "bsc": "5.50"}},
"all_time": {"confirmed": 4, "volume_usd": "1115.50", "by_chain": {"tron": "110.00", "bsc": "5.50", "bitcoin": "1000.00"}}
},
"generated_at": "2026-10-10T09:00:04.863003436Z"
}
| Field | Description |
|---|---|
livemode |
false for the test-mode figures (?mode=test) |
confirmed_today |
Payments confirmed since midnight UTC |
volume_usd_today |
Their total in dollars |
pending |
Open payments now: pending, partially_paid and detecting |
expired_today |
Expired payments (status: "expired") whose deadline passed since midnight UTC |
failed_webhooks |
Deliveries that gave up and wait for a redelivery |
by_chain |
The confirmed volume since midnight UTC per network, in dollars (the same window as volume_usd_today) |
periods |
Confirmed payments over four spans: today, last_7_days, last_30_days and all_time (below) |
generated_at |
When these figures were computed |
Each of periods has:
| Field | Description |
|---|---|
confirmed |
How many payments confirmed in the span |
volume_usd |
Their total in dollars, each at its own dollar value (usd_amount), 2 decimals |
by_chain |
That total per network, in dollars. Only networks with a confirmed payment in the span appear |
The spans are calendar days in UTC: today since midnight UTC,
last_7_days today and the 6 days before it, last_30_days today and the 29
before it, all_time every payment ever confirmed. Only confirmed payments
count, of the mode asked (?mode=test for test mode).
periods.today is the same as confirmed_today, volume_usd_today and
by_chain.
Event timeline
GET /merchant/payments/{id}/events?limit=100
Every event the payment raised, oldest first: the same events your webhook receives, kept even when no URL was set.
{
"events": [
{
"id": "9d4b2f7e1c6a3e08b5d2f1c4",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"payment_id": "2d9b6e14a7c3f0851e4b9a6c",
"type": "payment.expired",
"data": { "id": "2d9b6e14a7c3f0851e4b9a6c", "status": "expired", "…": "…" },
"created_at": "2026-10-10T09:15:04.117Z"
}
]
}
type is the event type, and data is exactly the data of the notification: the payment (or settlement) as it was at that moment, with the same field names.
A payment's trail
GET /merchant/payments/{id}/trail gathers everything payitgo recorded about a
payment, for when a customer says they paid, or paid twice, or you need to
show where the money went. The answer is {"trail": {…}} with:
| Field | Description |
|---|---|
payment |
The payment |
deposits |
Every transfer to it, with its sender, block and time |
unmatched_deposits |
Transfers made to its address (or destination tag) after it closed that no payment took |
events |
Its event timeline, as above |
webhook_deliveries |
Every notification sent about it, with every attempt: when, where, and your server's answer |
settlements |
Each deposit's {"settlement": …, "transfers": […]}: the settlement and every transaction that sent you money (and any it replaced) |
ledger |
Every ledger entry it made |
audit |
Who did what about it: the API key that created it, the customer's coin choice (with their IP), payitgo's actions |
generated_at |
When the trail was gathered |
Simulate a test payment
POST /merchant/payments/{id}/simulate · owner, developer, finance
The dashboard's way to simulate an outcome
on a test payment, from its page in test mode: the same as
POST /v1/payments/{id}/simulate, with an access token instead of an API key.
{id} is the payment id.
curl -s -X POST https://api.payitgo.co/merchant/payments/c41e7a9d2b05f3e8a6d17c90/simulate \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"outcome": "confirmed"}'
200 OK with the payment, with
"simulation": {"outcome": "confirmed", "state": "running"}. A live payment
answers 400 only test payments can be simulated: use a test key;
the outcomes, rules and every error are in Test mode.
A viewer gets 403 {"error": "insufficient permissions"}.
Audit log
GET /merchant/audit · owner
The account's record of who did what, newest first and never changed:
sign-ins and failed sign-ins, team and role changes, API keys, the webhook URL
and secret, payout addresses (before and after), settings, payouts, payments
created, your referrals (referral.created,
referral.voided), and payitgo's actions on the account.
| Query parameter | Description |
|---|---|
action |
An exact action (webhook.url_set) or a prefix ending in a dot (webhook.) |
subject |
The id of the record acted on |
payment_id |
Entries about one payment |
actor_id |
Entries by one team member or API key |
from, to |
Dates, 2026-10-01 |
limit, offset |
Paging; default 100, up to 500 |
curl -s 'https://api.payitgo.co/merchant/audit?action=webhook.&limit=2' -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK:
{
"entries": [
{
"id": "8b2d4f6a1c3e5a7c9e0b2d4f",
"at": "2026-10-10T09:00:04.734Z",
"action": "webhook.url_set",
"actor": {
"kind": "api_key",
"id": "4f2a9c1e7b3d5a08c6e1d7b2",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"ip": "203.0.113.24",
"user_agent": "curl/8.18.0",
"request_id": "kT4pWq9ZrX2mNc7bLv5sHd8fJg3yAe6R"
},
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"subject": "3f8a1c27b94e05d6a2c71e90:test",
"before": { "url": null },
"after": { "url": "https://acme.example/payitgo/ipn" }
},
{
"id": "1e3c5a7f9b2d4f6a8c0e1b3d",
"at": "2026-10-10T08:59:29.907Z",
"action": "webhook.secret_rolled",
"actor": {
"kind": "merchant",
"id": "9b2e4c71d8a05f3e6c1a7b24",
"email": "you@acme.example",
"role": "owner",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"ip": "203.0.113.24",
"user_agent": "Mozilla/5.0 …",
"request_id": "Qm7sVb2nXc9kLp4wZr6tHy3dFg8jAe5N"
},
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"subject": "3f8a1c27b94e05d6a2c71e90:test",
"note": "signing secret replaced"
}
],
"total": 6,
"limit": 2,
"offset": 0
}
| Field | Description |
|---|---|
action |
What happened, <what>.<verb>: webhook.url_set, webhook.url_removed, webhook.secret_rolled, payout_address.updated, payment.created… |
actor.kind |
Who: merchant (a team member), api_key, customer (a payer on the checkout), admin (payitgo's team, without the person's name), system (payitgo itself) or anonymous (before sign-in) |
actor.ip, actor.user_agent, actor.request_id |
Where the request came from, and its X-Request-Id |
subject |
The record acted on (the webhook is <merchant id>:test or :live) |
before, after |
The values before and after. Secrets are never recorded |
note |
A short explanation |
A bad date answers 400 {"error": "from must be a date: 2006-01-02"}.
Balances and payouts
Where a confirmed payment's money goes depends on your settlement: with direct settlement it is sent to your payout address; in custody it is credited to your balance, in USDT on Tron, and you request payouts. With direct settlement, what payitgo could not send you stays in your balance, and a USDT balance is paid out to you automatically once you have a USDT address on BNB Smart Chain or Ethereum. Test payments credit your test balance, which is never paid out. Payouts are live only.
Referral commissions are not part of this balance: they build up in your referral balance, which is paid out on its own.
| Method | Path | Role |
|---|---|---|
GET |
/merchant/balances?mode=test |
any |
GET |
/merchant/ledger?mode=test · /merchant/ledger.csv |
any |
GET |
/merchant/settlements?mode=test |
any |
GET POST PATCH DELETE |
/merchant/payout-addresses |
read: any; change: owner, finance |
GET POST |
/merchant/payouts |
read: any; request: owner, finance |
GET |
/merchant/payouts/quote?address_id=&amount= |
owner, finance |
POST |
/merchant/payouts/{id}/cancel |
owner, finance |
Balances
GET /merchant/balances?mode=test
curl -s https://api.payitgo.co/merchant/balances -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK:
{
"balances": [
{ "token": "USDT_TRC20", "chain": "tron", "available": "1043.27", "pending_payout": "250" }
]
}
| Field | Type | Description |
|---|---|---|
token, chain |
string | The coin |
available |
decimal string | What you can request a payout of now |
pending_payout |
decimal string | What is held for payouts not sent yet, on the balance each one draws on: a payout in USDT on another network counts here (its from_token) |
An account with nothing credited yet gets {"balances": []}. Referral
commissions are not counted here.
Ledger
GET /merchant/ledger?mode=test&token=USDT_TRC20&limit=50&offset=0
Your statement: every movement on your balance, newest first. Referral commissions and referral payouts are not in it.
{
"entries": [
{ "id": "5f446eb968ee2af08be79772", "merchant_id": "3f8a1c27b94e05d6a2c71e90", "type": "network_fee", "token": "USDT_TRC20", "chain": "tron", "amount": "-3.212009", "usd_amount": "-3.212009", "reference": "8f3a2c1e9b7d4a6f05e1c2d3:c19240…51bd", "created_at": "2026-10-10T09:04:03.81Z" },
{ "id": "c921ff34b8f23cd4346464ae", "merchant_id": "3f8a1c27b94e05d6a2c71e90", "type": "fee", "token": "USDT_TRC20", "chain": "tron", "amount": "-0.499", "usd_amount": "-0.499", "reference": "8f3a2c1e9b7d4a6f05e1c2d3:c19240…51bd", "created_at": "2026-10-10T09:04:03.81Z" },
{ "id": "23eb41673da1f63b49577d2b", "merchant_id": "3f8a1c27b94e05d6a2c71e90", "type": "payment_credit", "token": "USDT_TRC20", "chain": "tron", "amount": "49.9", "usd_amount": "49.9", "reference": "8f3a2c1e9b7d4a6f05e1c2d3:c19240…51bd", "created_at": "2026-10-10T09:04:03.81Z" }
],
"total": 3,
"limit": 50,
"offset": 0
}
The ledger entry object
| Field | Type | Description |
|---|---|---|
id |
string | The entry's id |
merchant_id |
string | Your account |
type |
string | What it is (below) |
token, chain |
string | The coin |
amount |
decimal string | Signed: credits positive, debits negative |
usd_amount |
decimal string | Its dollar value at the time |
reference |
string | <payment id>:<transaction id> for a deposit, or the payout id |
note |
string, optional | Details, such as what a converted deposit was |
created_at |
string | When it was recorded |
type |
Meaning |
|---|---|
payment_credit |
A final deposit to one of your payments |
fee |
payitgo's service fee on that deposit |
network_fee |
The network fee of moving that deposit |
conversion_fee |
The fee for converting the deposit into another coin |
conversion_out · conversion_in |
The deposit leaving your balance to be converted (noted converting into USDT_TRC20 while an exchange converts it), and what the conversion bought coming back |
settlement |
Direct settlement: your share leaving for your own wallet (converted into USDT_TRC20 and sent to your wallet when it was converted across networks on the way) |
settlement_return |
A direct settlement that could not be sent, kept in your balance instead |
payout_hold · payout_release |
A payout request holding the amount, and a rejected or cancelled payout giving it back, in the balance the payout draws on (its from_token when it has one) |
adjustment |
A correction made by payitgo's team |
Your share of payitgo's fee on a referred merchant's payment (an
affiliate_commission), and the payouts of it, are neither listed here nor
counted in GET /merchant/balances. They make your
referral balance: its history is
GET /merchant/affiliate/commissions and
GET /merchant/affiliate/payouts.
GET /merchant/ledger.csv takes the same mode and token and returns the
whole statement, newest first, as a spreadsheet, with the columns id, created_at, type, token, chain, amount, usd_amount, reference, note. Like
every dashboard route it needs the Authorization header (an access token is
never accepted in the URL), so a download button fetches it and saves the
response.
Payout addresses
Your wallets: where payouts go, and, with direct settlement, where each coin's
payments are sent. Add one for each coin you accept: that coin's payments are
then sent to you as they arrive, with no conversion, at the lowest fee for
your customers; a coin without one is converted into one of your payout
coins first, and costs them more
(why). Test addresses (?mode=test) receive test settlements,
on paper: nothing is ever sent to them. A Bitcoin or Litecoin test address
may be in the mainnet or the testnet format (tb1…, m…, n…, tltc1…).
Adding an address or changing one asks for your password again.
| Method | Path | Role | Body | Response |
|---|---|---|---|---|
GET |
/merchant/payout-addresses?mode=test |
any | 200 {"addresses": [address, …]} |
|
POST |
/merchant/payout-addresses?mode=test |
owner, finance | token, address, tag, label, password |
201 {"address": address} |
PATCH |
/merchant/payout-addresses/{id} |
owner, finance | any of address, tag, label, default, and password when address or tag change |
200 {"address": address} |
DELETE |
/merchant/payout-addresses/{id} |
owner, finance | 200 {"message": "address deleted"} |
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | yes (POST) | The coin this address receives, such as USDT_TRC20. It cannot change later |
address |
string | yes (POST) | The wallet address, checked for the token's network |
tag |
integer | no | XRP only: the destination tag your exchange asks for |
label |
string | no | Your name for it |
default |
boolean | no (PATCH) | true makes it the address direct settlement pays for its coin |
password |
string | when adding, or changing address or tag |
Your sign-in password |
curl -s https://api.payitgo.co/merchant/payout-addresses \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"token": "USDT_TRC20", "address": "TWd4Kp9mR2xH7sN3vB6cJ8fL5qY1tZgEaU", "label": "Treasury", "password": "a-long-password"}'
201 Created:
{
"address": {
"id": "3e7a1c9f5b2d4e8a6c0f2b1d",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"livemode": true,
"token": "USDT_TRC20",
"chain": "tron",
"address": "TWd4Kp9mR2xH7sN3vB6cJ8fL5qY1tZgEaU",
"label": "Treasury",
"default": true,
"active_at": "2026-10-11T09:00:29.688370009Z",
"created_by": "9b2e4c71d8a05f3e6c1a7b24",
"created_at": "2026-10-10T09:00:29.688370009Z"
}
}
| Field | Description |
|---|---|
id |
The address's id |
livemode |
false for a test address |
token, chain, address, tag, label |
As you set them |
default |
Whether direct settlement pays this coin here. The first address of a coin is its default |
active_at |
When direct settlement starts using it. Your first live address of a coin: at once, as there was nowhere else for those payments to go. Any address that could redirect payments (another address of a coin, a changed address or tag, one added after the coin's last address was deleted): 24 hours later, so whoever might take over your account cannot redirect your payments at once; payments in its coin wait for it meanwhile, and are then sent to it as they are. Test addresses are active at once |
created_by, created_at |
Who added it, and when |
| Status | Message | Cause |
|---|---|---|
400 |
unsupported token in live mode |
Not a token of that mode |
400 |
address is not a valid tron address |
The address is not valid for the token's network |
400 |
only XRP addresses take a destination tag |
tag on a non-XRP address |
400 |
nothing to update |
An empty PATCH |
403 |
password is incorrect |
Wrong or missing password |
404 |
not found |
No such address |
Payouts
Ask for your balance to be sent to one of your live payout addresses. payitgo reviews each payout you request, then sends it: one in USDT on BNB Smart Chain or Ethereum by itself, usually within minutes of approving it, any other by payitgo's team. A USDT balance can be paid to your USDT address on any network (below). Referral commissions are not paid out here: ask for a referral payout.
Quote a payout
GET /merchant/payouts/quote?address_id=…&amount=… · owner, finance
What a payout to one of your live addresses would be, before you request
it; nothing is held. amount is optional: without it you get the network
fee and the minimum payout only. The answer is {"quote": {...}} with
address_id, token, chain, network_fee and network_fee_usd (what
sending it costs now), min_payout and min_payout_usd, and for an amount
amount, amount_usd, send_amount and send_amount_usd (what reaches
the address) and from_token, from_chain (the balance it would draw on,
when not the address's own coin's). It answers 400 for what a request
would refuse: under the minimum, an amount the fee eats, too little balance.
Fees move: the request's own answer has the final fee.
POST /merchant/payouts · owner, finance
| Field | Type | Required | Description |
|---|---|---|---|
address_id |
string | yes | A live payout address's id |
amount |
number or decimal string | yes | How much of the address's coin to pay out, at least $10 worth. The network fee of sending it comes out of this amount. Taken from your balance of that coin; for a USDT address, from another of your USDT balances when that one has too little |
curl -s https://api.payitgo.co/merchant/payouts \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"address_id": "3e7a1c9f5b2d4e8a6c0f2b1d", "amount": "250"}'
201 Created. The amount is held from your balance at once:
{
"payout": {
"id": "7e2c9a4f1b3d8e6a05c2f1d9",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"token": "USDT_TRC20",
"chain": "tron",
"amount": "250",
"address_id": "3e7a1c9f5b2d4e8a6c0f2b1d",
"address": "TWd4Kp9mR2xH7sN3vB6cJ8fL5qY1tZgEaU",
"network_fee": "3.21",
"network_fee_usd": "3.21",
"send_amount": "246.79",
"usd_rate": "1",
"amount_usd": "250",
"send_amount_usd": "246.79",
"status": "requested",
"requested_by": "9b2e4c71d8a05f3e6c1a7b24",
"created_at": "2026-10-10T10:00:00.31Z",
"updated_at": "2026-10-10T10:00:00.31Z"
}
}
Paying USDT out on another network
USDT is worth $1 on every network, so a USDT balance pays a payout to any of
your USDT payout addresses: USDT_TRC20, USDT_BEP20 or USDT_ERC20. Your
custody balance, held in USDT on Tron, can go to your USDT address on BNB
Smart Chain, where sending it costs about a cent instead of a few dollars on
Tron.
The request is the same (address_id, amount), and payitgo picks the
balance: the address's own coin when it has enough, otherwise another USDT
balance that has, USDT on Tron first. One balance pays the whole payout: it
is never split. The payout says which in from_token and from_chain, while
token and chain are the coin and network it is paid in. The minimum
payout ($10) applies as ever, the network fee is that of the network it is
paid on, and the amount is held from the balance drawn on (its
pending_payout, and the ledger's payout_hold).
The same 250 USDT from your Tron balance, to your USDT address on BNB Smart Chain:
{
"payout": {
"id": "5b9d1f3a7c2e4b6d8f0a1c3e",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"token": "USDT_BEP20",
"chain": "bsc",
"amount": "250",
"from_token": "USDT_TRC20",
"from_chain": "tron",
"address_id": "8c4e2a6f1b3d5e7a9c0b2d4f",
"address": "0x4b7e2d9a1c6f3e8b5a0d7c2e9f4a1b6d3c8e5f2a",
"network_fee": "0.00412345",
"network_fee_usd": "0.01",
"send_amount": "249.99587655",
"usd_rate": "1",
"amount_usd": "250",
"send_amount_usd": "249.99",
"status": "requested",
"requested_by": "9b2e4c71d8a05f3e6c1a7b24",
"created_at": "2026-10-10T10:00:00.31Z",
"updated_at": "2026-10-10T10:00:00.31Z"
}
}
Automatic payouts
With direct settlement, what payitgo could not send you stays in your
balance (when).
payitgo pays a USDT balance out to you by itself, as one payout of the whole
available: to your USDT address on BNB Smart Chain that direct settlement
uses (past its active_at; your default one first), else on Ethereum. It
looks every 10 minutes, and pays as soon as it can send the payout. Such a
payout has requested_by: "auto", is approved at once and sent by payitgo
for the network's fee (about a cent on BNB Smart Chain), with the usual
payout.approved and payout.sent webhooks.
Nothing is paid out automatically under the minimum payout ($10), to an address still in its 24 hours, or without a USDT address on BNB Smart Chain or Ethereum: request a payout of it as usual (to your USDT address on Tron, say). Custody waits for your requests, and scheduled settlement keeps its schedule.
The payout object
| Field | Type | Description |
|---|---|---|
id |
string | The payout's id |
token, chain |
string | The coin paid out, and its network |
from_token, from_chain |
string, optional | The balance it draws on, when that is not token's: a USDT balance paid out as USDT on another network (USDT_TRC20, tron for your custody balance paid to USDT_BEP20). Absent when it draws on its own coin |
amount |
decimal string | Taken from your balance (from_token's when present) |
address_id, address, tag |
string | Where it goes, fixed when you requested it (changing the payout address later does not move it) |
network_fee, network_fee_usd |
decimal string | The network fee of sending it, quoted at the request, in the coin and in dollars |
send_amount |
decimal string | What reaches your address: amount less network_fee |
usd_rate |
decimal string | The coin's dollar price when you requested the payout, to 8 decimals ("1" for USDT) |
amount_usd, send_amount_usd |
decimal string | amount in dollars at usd_rate, to the cent, and what network_fee_usd leaves of it, so the three add up |
status |
string | requested → approved → sent; or rejected (by payitgo) or canceled (by you), which return the amount to your balance |
tx_id |
string, optional | The transaction, once sent |
review_note |
string, optional | payitgo's note, for example why it was rejected |
requested_by |
string | Who asked for it: a user's id, scheduler for a scheduled settlement's payout, or auto for a balance paid out automatically |
approved_at, rejected_at, canceled_at, sent_at |
string, optional | When each step happened |
canceled_by |
string, optional | Who cancelled it |
created_at, updated_at |
string | When it was requested and last changed |
Each step payitgo takes raises a webhook to your live account webhook URL,
with this object as data: payout.approved, payout.rejected,
payout.sent (with tx_id).
| Method | Path | Response |
|---|---|---|
GET |
/merchant/payouts?status=&limit=&offset= |
200 {"payouts": [payout, …], "total": n, "limit": 50, "offset": 0} |
GET |
/merchant/payouts/{id} |
200 {"payout": payout} |
POST |
/merchant/payouts/{id}/cancel |
200 {"payout": payout} with "status": "canceled", while it is still requested |
| Status | Message | Cause |
|---|---|---|
400 |
amount must be > 0 |
Zero or negative |
400 |
unknown payout address (test balances are not paid out) |
Not one of your live addresses |
400 |
the minimum payout is $10.00 (about 10 USDT_TRC20) |
Under $10 worth |
400 |
amount must be more than the network fee of 3.21 USDT_TRC20 |
Nothing would be left to send |
400 |
insufficient available balance |
More than available; for a USDT address, more than any one of your USDT balances has |
404 |
not found |
No such payout |
409 |
payout is not in a state that allows this |
Cancelling a payout that is no longer requested |
Support chat
Your account has one chat with payitgo's support, shared by the whole team: every role can read it and write in it, and everyone sees every message, whoever on the team wrote it. A message is text, up to 5000 characters (spaces at either end are trimmed). You send it over REST, and every open dashboard receives it live, support's replies included: there is nothing to poll.
| Method | Path | Role | Body | Response |
|---|---|---|---|---|
GET |
/merchant/support?before=&limit= |
any | 200 {"messages": [message, …], "more": false, "chat": chat} |
|
GET |
/merchant/support/unread |
any | 200 {"unread": 1} |
|
POST |
/merchant/support/messages |
any | text |
201 {"message": message} |
POST |
/merchant/support/read |
any | 200 {"chat": chat} |
|
PATCH |
/merchant/support/messages/{id} |
its author | text |
200 {"message": message} |
DELETE |
/merchant/support/messages/{id} |
its author | 200 {"message": message} |
Sending and editing are limited to one message a second per person, in bursts of up to 5.
curl -s https://api.payitgo.co/merchant/support/messages \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"text": "Hello, our webhook deliveries have been failing since this morning. Is anything wrong on your side?"}'
201 Created:
{
"message": {
"id": "4c9e1a7f3b2d8e6a05f1c2d9",
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"author": "merchant",
"author_id": "5e8a2c9d1f4b7036a2e9c1d8",
"author_name": "dev",
"text": "Hello, our webhook deliveries have been failing since this morning. Is anything wrong on your side?",
"created_at": "2026-10-14T09:12:03.118Z"
}
}
Reading the chat
GET /merchant/support?before=&limit= · any role
The latest messages, oldest first. more is true when older ones remain:
ask for them with before=<the first message's id>. limit is 1 to 100
(default 50).
{
"messages": [
{ "id": "4c9e1a7f3b2d8e6a05f1c2d9", "merchant_id": "3f8a1c27b94e05d6a2c71e90", "author": "merchant", "author_id": "5e8a2c9d1f4b7036a2e9c1d8", "author_name": "dev", "text": "Hello, our webhook deliveries have been failing since this morning. Is anything wrong on your side?", "created_at": "2026-10-14T09:12:03.118Z" },
{ "id": "8b2d6f0a4c1e9a3d7f5b2c6e", "merchant_id": "3f8a1c27b94e05d6a2c71e90", "author": "support", "author_name": "Maya", "text": "Hi! Nothing on ours: your server answers 502. Deliveries retry on their own, and you can redeliver the failed ones once it is fixed.", "created_at": "2026-10-14T09:15:40.502Z" }
],
"more": false,
"chat": { "agent_name": "Maya", "waiting": false, "support_read_at": "2026-10-14T09:15:40.502Z", "unread": 1, "messages": 2 }
}
Reading marks nothing read, so a dashboard can load the chat, or show a
badge with GET /merchant/support/unread ({"unread": 1}: support's
messages your team has not read), without support seeing it read. When
someone has the chat open on screen, call POST /merchant/support/read:
support sees your team read it (merchant_read_at), and unread goes back
to 0 for the whole team. Before its first message the chat is empty:
"messages": [], with "waiting": false and "unread": 0.
The message object
| Field | Type | Description |
|---|---|---|
id |
string | The message's id |
merchant_id |
string | Your account |
author |
string | merchant (someone on your team) or support |
author_id |
string, optional | The team member who wrote it (their id); absent on support's messages |
author_name |
string | Their name (for a member without one, their email address before the @), or the support agent's |
text |
string | The message; "" once deleted |
created_at |
string | When it was sent |
edited_at |
string, optional | When it was last edited |
deleted_at |
string, optional | When it was deleted |
The chat object
| Field | Type | Description |
|---|---|---|
agent_name |
string, optional | The support agent who has your chat |
waiting |
boolean | true from your team's message until support replies |
merchant_read_at |
string, optional | When your team last marked the chat read |
support_read_at |
string, optional | When support last read it: your messages sent before then have been seen |
unread |
integer | Support's messages your team has not read |
messages |
integer | How many messages the chat holds, deleted ones included |
A support agent takes your chat when you write (agent_name) and answers
it. Support sees every message with its writer's name, your account's name,
when your team last read the chat, and when someone on your team is typing.
When support replies while nobody on your team has the dashboard open (no
socket connected), the team is told: a notification in
the support category, in the bell of every role and by email to those who
chose it (on by default), with the reply's first 300 characters. At most one
every 10 minutes.
Editing and deleting
PATCH /merchant/support/messages/{id} {"text": "…"} and
DELETE /merchant/support/messages/{id} · the message's author
Each person edits and deletes only their own messages. An edit sets
edited_at. A deleted message stays in the chat with "text": "" and
deleted_at: show it as deleted. Deleting it again changes nothing, and it
can no longer be edited. payitgo keeps what a message said before each edit
and before it was deleted, for disputes; nobody is shown it.
| Status | Message | Cause |
|---|---|---|
400 |
text is required · text is too long (5012 characters; up to 5000) |
An empty message, or over 5000 characters |
403 |
only its author can change a message |
Editing or deleting someone else's message |
404 |
message not found |
No such message in your chat (also a before that is none) |
409 |
this message was deleted |
Editing a deleted message |
429 |
rate limit exceeded |
More than one message a second, past a burst of 5 |
Live chat updates
Every team member's dashboard socket receives:
| Event | Payload |
|---|---|
support:message |
A new message: a team member's (the sender's own included: upsert it by id) or support's |
support:message:update |
A message edited or deleted: the whole message, to replace the one shown |
support:read |
{"side", "read_at", "unread"}. side support: support read the chat up to read_at. merchant: a team member marked it read, and the badge is back to 0 |
support:typing |
{"side": "support", "name", "typing"}: someone at support started (true) or stopped typing |
The dashboard tells support someone is typing with an event of its own,
support:typing: true (or {"typing": true}) while someone types in the
chat box, false when they stop or send. Support sees the person's name
while they type. It is acknowledged with {"ok": true}. A true sent less
than a second after the same person's last true is not passed on again
(support is already shown them typing), so there is no need to send it more
than once a second; false always goes through.
socket.on("support:message", (m) => upsertMessage(m));
socket.on("support:message:update", (m) => upsertMessage(m));
socket.on("support:read", (r) => (r.side === "support" ? showSeen(r.read_at) : setBadge(0)));
socket.on("support:typing", (t) => showTyping(t.typing ? `${t.name} is typing…` : null));
// while someone types (at most once a second), and when they stop or send
await socket.emitWithAck("support:typing", true); // {"ok": true}
await socket.emitWithAck("support:typing", false);
Referral program
Every payitgo account can refer other businesses and earn from what they process. Share your referral link: a business that opens an account through it is your referral, and each of its live payments earns you a share of payitgo's service fee on it:
- 25% of the service fee on each live payment of a merchant you referred,
- 30% once you have 5 active referrals (referrals whose payments earned you a commission in the last 30 days),
- for 5 years from each referred merchant's signup,
- credited to your referral balance in USDT on Tron as each payment's fee is final. That balance is apart from your account's own balance: ask for it to be paid out, from $50, and payitgo pays you within 5 business days (Referral balance and payouts).
Only the service fee is shared, never the network fee or a conversion fee.
payitgo may agree other terms with you (a rate of your own, a longer time,
or for as long as your referrals use payitgo): GET /merchant/affiliate
always shows the terms in force.
| Method | Path | Role | Response |
|---|---|---|---|
GET |
/merchant/affiliate |
owner, finance | 200 your code, link, terms, figures and referral balance |
GET |
/merchant/affiliate/referrals?limit=&offset= |
owner, finance | 200 {"referrals": [referral, …], "total": n, "limit": 50, "offset": 0}, newest first |
GET |
/merchant/affiliate/commissions?limit=&offset= |
owner, finance | 200 {"commissions": [commission, …], "total": n, "limit": 50, "offset": 0}, newest first |
POST |
/merchant/affiliate/payouts |
owner, finance | 201 {"payout": payout} |
GET |
/merchant/affiliate/payouts?status=&limit=&offset= |
owner, finance | 200 {"payouts": [payout, …], "total": n, "limit": 50, "offset": 0}, newest first |
GET |
/merchant/affiliate/payouts/{id} |
owner, finance | 200 {"payout": payout} |
POST |
/merchant/affiliate/payouts/{id}/cancel |
owner, finance | 200 {"payout": payout}, while it is requested |
POST |
/referrals/{code}/click |
no authentication | 200 {"valid": true} |
The referral program is live only: these routes take no mode.
Your code and link
GET /merchant/affiliate · owner, finance
The first call gives your account its referral code: 8 characters,
lower-case letters and digits without the ones easy to misread (0, o,
1, l, i). It never changes. link is what you share: payitgo's signup
page with ?ref=<code>. The answer also has your referral balance and how
it is paid out.
curl -s https://api.payitgo.co/merchant/affiliate -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK:
{
"affiliate": {
"merchant_id": "3f8a1c27b94e05d6a2c71e90",
"merchant_name": "Acme Store",
"code": "k7m2x9qa",
"status": "active",
"lifetime": false,
"created_at": "2026-10-11T14:20:07.331Z",
"updated_at": "2026-10-11T14:20:07.331Z"
},
"link": "https://payitgo.co/signup?ref=k7m2x9qa",
"terms": {
"rate_bps": 2500,
"custom": false,
"commission_bps": 2500,
"boosted_bps": 3000,
"boost_referrals": 5,
"active_days": 30,
"duration_months": 60,
"lifetime": false
},
"stats": {
"clicks": 214,
"clicks_30d": 61,
"referrals": 3,
"referrals_30d": 1,
"active_referrals": 2,
"payments": 148,
"volume_usd": "58240.75",
"earned_usd": "145.601875",
"earned_30d_usd": "12.415"
},
"balance": {
"token": "USDT_TRC20",
"chain": "tron",
"available": "45.601875",
"pending_payout": "100",
"paid_out": "0",
"earned": "145.601875"
},
"payouts": {
"min_payout_usd": "50",
"network_fee": "1.5",
"payout_days": 5,
"open": {
"id": "9dfe8f5ae04f2aeab89957db",
"affiliate_id": "3f8a1c27b94e05d6a2c71e90",
"merchant_name": "Acme Store",
"token": "USDT_TRC20",
"chain": "tron",
"amount": "100",
"network_fee": "1.5",
"send_amount": "98.5",
"address_id": "3e7a1c9f5b2d4e8a6c0f2b1d",
"address": "TWd4Kp9mR2xH7sN3vB6cJ8fL5qY1tZgEaU",
"status": "requested",
"due_at": "2026-10-22T06:00:41.03Z",
"requested_by": "9b2e4c71d8a05f3e6c1a7b24",
"created_at": "2026-10-15T06:00:41.03Z",
"updated_at": "2026-10-15T06:00:41.03Z"
}
}
}
| Field | Description |
|---|---|
affiliate.code, link |
Your referral code, and the link to share |
affiliate.status |
active, or suspended by payitgo: your link is refused and your referrals earn nothing more |
affiliate.commission_bps, affiliate.duration_months, affiliate.lifetime |
Terms of your own, when payitgo agreed them with you |
terms.rate_bps |
What your referrals' payments earn you now, in basis points of payitgo's service fee (2500 = 25%) |
terms.custom |
true with a rate of your own, which is never boosted |
terms.commission_bps, terms.boosted_bps |
The rate, and the rate once you have terms.boost_referrals active referrals: referrals that earned you a commission in the last terms.active_days days |
terms.duration_months, terms.lifetime |
How long each referral earns you, from its signup; lifetime: for as long as it uses payitgo |
stats.clicks, stats.clicks_30d |
Visits through your link, ever and in the last 30 days |
stats.referrals, stats.referrals_30d, stats.active_referrals |
Merchants who signed up through it, ever and in the last 30 days, and how many are active |
stats.payments, stats.volume_usd |
Your referrals' live deposits that earned you a commission, and their total in dollars |
stats.earned_usd, stats.earned_30d_usd |
What you earned, ever and in the last 30 days, in dollars (USDT) |
balance.token, balance.chain |
Always USDT_TRC20, tron: your referral balance is held in USDT on Tron (1 USDT = $1) |
balance.available |
What you can ask to be paid now |
balance.pending_payout |
Asked for and not paid yet (a payout requested or processing) |
balance.paid_out |
Paid to you so far |
balance.earned |
Every commission ever: available + pending_payout + paid_out |
payouts.min_payout_usd |
The least you can ask to be paid, in USDT |
payouts.network_fee |
What sending a payout costs now, in USDT, taken from the payout. A quote: each payout's fee is fixed when you request it. "0" when it could not be quoted just now |
payouts.payout_days |
The business days (Monday to Friday) within which payitgo pays a request |
payouts.open |
Your payout that is requested or processing, if any; absent when none. You have one open at most |
A developer or a viewer gets 403 {"error": "insufficient permissions"}.
Signing up through a link
When someone opens your link, the signup page calls
POST /referrals/{code}/click (no authentication, any origin, 5 a second
per IP address). It counts the visit in your clicks and answers
{"valid": true} when the code is an active affiliate's, or
{"valid": false} (and counts nothing) for an unknown or suspended code. The
page keeps the code and sends it as referral_code when the account is
opened (Open an account).
curl -s -X POST https://api.payitgo.co/referrals/k7m2x9qa/click
The new account is your referral from then on. Owners and finance get an
affiliate:referral event and a "New referral" notification in the
affiliates category (emailed to them by default).
What earns nothing
- Test payments.
- A referral's payments after its
earns_until. - Referring yourself. An account opened through your link that shares a live
payout address with yours (the same address, in any case) is taken for your
own: at its next payment its referral is voided (
"status": "void","void_reason": "it shares a payout address with its affiliate") and earns nothing from then on. payitgo can also void a referral it finds abusive, with the reason. - Anything while your affiliate
statusissuspended.
Referrals
GET /merchant/affiliate/referrals?limit=50&offset=0 · owner, finance
The merchants you referred, newest first, with what each earned you.
{
"referrals": [
{
"merchant_id": "7c1e5a9d3b2f4086e1a7c5d9",
"merchant_name": "Globex Games",
"affiliate_id": "3f8a1c27b94e05d6a2c71e90",
"code": "k7m2x9qa",
"signed_up_at": "2026-10-12T08:41:55.120Z",
"status": "active",
"created_at": "2026-10-12T08:41:55.297Z",
"earns_until": "2031-10-12T08:41:55.120Z",
"payments": 37,
"volume_usd": "4210.5",
"earned_usd": "10.52625",
"last_earned_at": "2026-10-14T10:02:11.402Z"
}
],
"total": 3,
"limit": 50,
"offset": 0
}
The referral object
| Field | Type | Description |
|---|---|---|
merchant_id, merchant_name |
string | The merchant you referred |
affiliate_id |
string | Your account |
code |
string | The code it signed up with |
signed_up_at |
string | When it opened its account |
status |
string | active, or void: it earns you nothing more |
void_reason, voided_at |
string, optional | Why and when it was voided |
attached_by |
string, optional | Present when payitgo attached the merchant to your code by hand |
created_at |
string | When it became your referral |
earns_until |
string or null | When its payments stop earning you commissions; null: for as long as it uses payitgo |
payments |
integer | Its live deposits that earned you a commission |
volume_usd, earned_usd |
decimal string | Their total in dollars, and what they earned you |
last_earned_at |
string, optional | Its latest commission |
Commissions
GET /merchant/affiliate/commissions?limit=50&offset=0 · owner, finance
Once a live deposit to a merchant you referred is settled and payitgo's fee on it is final, your share of that fee is credited to your referral balance in USDT on Tron: one commission per deposit. It is not in your account's balance or ledger; ask for a referral payout to be paid it. Newest first:
{
"commissions": [
{
"id": "5b8e2a1c9d4f7e3a06c2b1d8:4be17c…9c0a",
"affiliate_id": "3f8a1c27b94e05d6a2c71e90",
"merchant_id": "7c1e5a9d3b2f4086e1a7c5d9",
"merchant_name": "Globex Games",
"payment_id": "5b8e2a1c9d4f7e3a06c2b1d8",
"deposit_usd": "120",
"fee_usd": "1.2",
"rate_bps": 2500,
"token": "USDT_TRC20",
"amount": "0.3",
"created_at": "2026-10-14T10:02:11.402Z"
}
],
"total": 148,
"limit": 50,
"offset": 0
}
The commission object
| Field | Type | Description |
|---|---|---|
id |
string | The deposit it comes from, <payment id>:<transaction id> |
affiliate_id |
string | Your account |
merchant_id, merchant_name |
string | The referred merchant who was paid |
payment_id |
string | Their payment |
deposit_usd |
decimal string | The deposit, in dollars |
fee_usd |
decimal string | payitgo's service fee on it, in dollars |
rate_bps |
integer | Your share of the fee, in basis points |
token |
string | USDT_TRC20 |
amount |
decimal string | What was credited to your referral balance: the fee times your rate |
created_at |
string | When it was credited |
Referral balance and payouts
Your commissions build up in your referral balance, in USDT on Tron
(USDT_TRC20), apart from your account's own balance:
GET /merchant/balances and the ledger never show
them, and a payout of your balance cannot take them.
GET /merchant/affiliate gives the balance (balance) and the payout terms
(payouts) (Your code and link).
- Ask for a payout of all of
balance.available, or part of it, frompayouts.min_payout_usd($50 by default). - It is paid to one of your live USDT (Tron) payout addresses, your default one unless you choose another, or to any other Tron address you give with the request, with your password.
- The network fee of sending it comes out of it:
network_feeis fixed when you ask, andsend_amountis what reaches the address. - The amount leaves
availableat once, and is inpending_payoutuntil it is paid. - payitgo pays it within
payouts.payout_daysbusiness days (5 by default, Monday to Friday): itsdue_at. - You have one payout open at a time. You can cancel it while it is
requested: the amount goes back toavailable. - Referral payouts are paid in USDT on Tron only.
Request a referral payout
POST /merchant/affiliate/payouts · owner, finance · 1 a second per user,
in bursts of 5
| Field | Type | Required | Description |
|---|---|---|---|
amount |
number or decimal string | no | How much of balance.available, at most 6 decimals. Absent or 0: all of it |
address_id |
string | no | One of your live payout addresses, a USDT_TRC20 one |
address |
string | no | Any other Tron address (T…, 34 characters) |
password |
string | with address |
Your sign-in password: the money goes somewhere that is not one of your payout addresses |
Give address_id or address, not both. With neither, the payout goes to
your default USDT_TRC20 payout address (the oldest one when none is marked
default).
curl -s https://api.payitgo.co/merchant/affiliate/payouts \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"amount": "100"}'
201 Created. The amount is held from your referral balance at once:
{
"payout": {
"id": "9dfe8f5ae04f2aeab89957db",
"affiliate_id": "3f8a1c27b94e05d6a2c71e90",
"merchant_name": "Acme Store",
"token": "USDT_TRC20",
"chain": "tron",
"amount": "100",
"network_fee": "1.5",
"send_amount": "98.5",
"address_id": "3e7a1c9f5b2d4e8a6c0f2b1d",
"address": "TWd4Kp9mR2xH7sN3vB6cJ8fL5qY1tZgEaU",
"status": "requested",
"due_at": "2026-10-22T06:00:41.03Z",
"requested_by": "9b2e4c71d8a05f3e6c1a7b24",
"created_at": "2026-10-15T06:00:41.03Z",
"updated_at": "2026-10-15T06:00:41.03Z"
}
}
To an address that is not one of your payout addresses, with your password
(the answer has no address_id then):
curl -s https://api.payitgo.co/merchant/affiliate/payouts \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"amount": "100", "address": "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK", "password": "a-long-password"}'
Owners and finance are told of every request: a "Payout requested"
notification in the affiliates category, emailed by default, saying how
much, to which address and by when payitgo pays it. If nobody of your team
asked for it, cancel it and change your password.
Errors, in the order they are checked:
| Status | Message | Cause |
|---|---|---|
403 |
insufficient permissions |
A developer or a viewer |
429 |
rate limit exceeded |
More than 1 request a second |
400 |
invalid request body |
Not JSON, or amount is not a number |
403 |
password is incorrect |
address without your right password |
409 |
a payout is open already: cancel it, or wait for it to be paid |
You have a payout requested or processing |
400 |
give address_id or address, not both |
|
400 |
address is not a valid Tron address |
|
400 |
referral payouts are paid in USDT on Tron: choose a USDT (Tron) payout address |
address_id is an address of another coin |
400 |
unknown payout address |
address_id is not one of your live payout addresses |
400 |
add a USDT (Tron) payout address, or give the address to pay |
Neither given, and you have no live USDT_TRC20 payout address |
400 |
amount must be more than 0 |
Negative |
400 |
amount has more than 6 decimals |
|
400 |
the minimum payout is 50.00 USDT_TRC20 |
Under payouts.min_payout_usd; also when you ask for all of it and available is under the minimum |
400 |
amount must be more than the network fee of 1.5 USDT_TRC20 |
Nothing would be left to send |
400 |
your referral balance holds 45.601875 USDT_TRC20 |
More than available |
500 |
could not request the payout |
For example the network fee could not be quoted: try again a little later |
The referral payout object
| Field | Type | Description |
|---|---|---|
id |
string | The payout's id |
affiliate_id, merchant_name |
string | Your account |
token, chain |
string | Always USDT_TRC20, tron |
amount |
decimal string | Taken from your referral balance |
network_fee |
decimal string | The network fee of sending it, fixed when you asked, taken from amount |
send_amount |
decimal string | What reaches address: amount less network_fee |
address_id |
string, optional | The payout address you chose (or your default); absent for an address given with the request |
address |
string | The Tron address it is paid to, fixed when you asked (changing your payout address later does not move it) |
status |
string | requested, processing, paid, rejected or canceled (below) |
due_at |
string | When payitgo pays it by: payout_days business days after the request, at the same time of day (UTC; Saturdays and Sundays skipped) |
requested_by |
string | The user who asked for it |
tx_id |
string, optional | The Tron transaction that paid it, once paid |
note |
string, optional | Why payitgo rejected it; sent by hand when payitgo paid it outside the usual flow |
approved_at |
string, optional | When payitgo started sending it |
paid_at, rejected_at |
string, optional | When it was paid, or rejected |
canceled_by, canceled_at |
string, optional | Who cancelled it, and when |
created_at, updated_at |
string | When it was requested and last changed |
status |
Means |
|---|---|
requested |
Waiting for payitgo. The amount is held: out of available, in pending_payout. You can cancel it |
processing |
payitgo is sending it: it can no longer be cancelled. It becomes paid once the transaction is on chain. Should it fail to be sent, payitgo may reject it, and the amount comes back |
paid |
Paid: tx_id is the transaction. The amount moves to paid_out |
rejected |
Refused by payitgo; note says why. The amount is back in available |
canceled |
Cancelled by your team. The amount is back in available |
paid, rejected and canceled are final.
Follow and cancel referral payouts
| Method | Path | Role | Response |
|---|---|---|---|
GET |
/merchant/affiliate/payouts?status=&limit=&offset= |
owner, finance | 200 {"payouts": [payout, …], "total": n, "limit": 50, "offset": 0}. Newest first; with status=requested or status=processing, soonest due first. limit 1 to 200 |
GET |
/merchant/affiliate/payouts/{id} |
owner, finance | 200 {"payout": payout} |
POST |
/merchant/affiliate/payouts/{id}/cancel |
owner, finance | 200 {"payout": payout} with "status": "canceled", while it is still requested. The amount is back in available |
curl -s https://api.payitgo.co/merchant/affiliate/payouts/9dfe8f5ae04f2aeab89957db -H "Authorization: Bearer $ACCESS_TOKEN"
200 OK, a payout payitgo has paid:
{
"payout": {
"id": "9dfe8f5ae04f2aeab89957db",
"affiliate_id": "3f8a1c27b94e05d6a2c71e90",
"merchant_name": "Acme Store",
"token": "USDT_TRC20",
"chain": "tron",
"amount": "100",
"network_fee": "1.5",
"send_amount": "98.5",
"address_id": "3e7a1c9f5b2d4e8a6c0f2b1d",
"address": "TWd4Kp9mR2xH7sN3vB6cJ8fL5qY1tZgEaU",
"status": "paid",
"due_at": "2026-10-22T06:00:41.03Z",
"requested_by": "9b2e4c71d8a05f3e6c1a7b24",
"tx_id": "9f3c5a0e7d2b41c8a6e5f0d3b2a1c9e8d7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2",
"approved_at": "2026-10-19T09:12:05.418Z",
"paid_at": "2026-10-19T09:13:02.771Z",
"created_at": "2026-10-15T06:00:41.03Z",
"updated_at": "2026-10-19T09:13:02.771Z"
}
}
curl -s -X POST https://api.payitgo.co/merchant/affiliate/payouts/9dfe8f5ae04f2aeab89957db/cancel \
-H "Authorization: Bearer $ACCESS_TOKEN"
| Status | Message | Cause |
|---|---|---|
403 |
insufficient permissions |
A developer or a viewer |
404 |
not found |
No such payout of yours |
409 |
payout is not in a state that allows this |
Cancelling a payout that is no longer requested |
500 |
could not list the payouts · could not read the payout · could not cancel the payout |
Besides "Payout requested", owners and finance get these affiliates
notifications:
| Title | When | Body |
|---|---|---|
| Payout paid | It is paid |
payitgo paid 98.5 USDT_TRC20 of your referral balance to TWd4Kp9mR2xH7sN3vB6cJ8fL5qY1tZgEaU (transaction 9f3c…a3b2). |
| Payout rejected | It is rejected |
Your payout of 100 USDT_TRC20 was rejected: <reason>. It is back in your referral balance. |
On the dashboard socket, owners and finance get
affiliate:balance (the balance object) whenever the referral balance
changes, and affiliate:payout (the payout) on every change of a payout.
Real-time events
A dashboard can stay live without polling: connect socket.io (a 4.x client)
to the /merchant namespace with an access token. Each event carries the same
object the REST route returns, so you upsert it by id. Test and live arrive
on the same connection: filter on livemode (mode on notifications).
import { io } from "socket.io-client";
// auth as a function: every (re)connection sends the access token you hold now
const socket = io("https://api.payitgo.co/merchant", { auth: (cb) => cb({ token: accessToken }), transports: ["websocket"] });
socket.on("payment:update", (e) => upsertPayment(e.payment)); // e.type: payment.created, payment.confirmed…
socket.on("dashboard:update", (d) => { if (d.livemode === live) showFigures(d); });
socket.on("auth:error", async ({ code }) => {
if (code === "token_expired") { await refreshAccessToken(); socket.connect(); } // the server has disconnected
else signOut(); // session_revoked, role_changed, removed, suspended
});
| Event | Payload | Who gets it |
|---|---|---|
payment:update |
{"type", "payment"}: the whole payment after each change, as GET /merchant/payments lists it. The checkout's event plus merchant_id and payment.created |
Everyone |
dashboard:update |
The dashboard figures, with livemode. At most one every 2 seconds per mode |
Everyone |
balance:update |
{"livemode", "balances"}, as GET /merchant/balances: your account's own balances, never the referral balance |
Everyone |
settlement:update |
A settlement, on every change | Everyone |
payout:update |
A payout: requested, canceled, approved, rejected, sent. It can also arrive with nothing you see changed (payitgo sending it): upsert it all the same | Everyone |
payout_address:update |
{"address", "deleted"} |
Everyone |
webhook:delivery |
A delivery, on every attempt | Everyone |
webhook:update |
The webhook, without its secret | Owners, developers |
notification, notification:read |
A new notification; the user read some or all of them (ids, all, livemode, unread) |
By category (support: everyone; affiliates: owners, finance); the user who read them |
merchant:update |
The merchant | Everyone |
store:update, api_key:update |
A store; an API key (never its secret) | Everyone; owners and developers |
team:update |
{"action", "user", "invitation"}: a member or an invitation changed |
Owners |
link:update, customer:update |
A payment link; a customer | Everyone |
audit:entry |
An audit log entry | Owners |
support:message, support:message:update |
A new support chat message; one edited or deleted, whole, to replace the one shown | Everyone |
support:read, support:typing |
Support or a team member read the chat ({"side", "read_at", "unread"}); someone at support started or stopped typing ({"side", "name", "typing"}) (Live chat updates) |
Everyone |
affiliate:referral, affiliate:commission |
A referral: a merchant signed up through your link, or a referral was voided; a commission just credited (an affiliate:balance follows) |
Owners, finance |
affiliate:balance |
Your referral balance, the balance object of GET /merchant/affiliate, whenever it changes: a commission credited, a payout requested, cancelled, rejected or paid |
Owners, finance |
affiliate:payout |
A referral payout, on every change: requested, cancelled, being sent (processing), paid, rejected. It can also arrive with nothing you see changed (payitgo's transfer moving on): upsert it all the same |
Owners, finance |
Besides auth:refresh (below), the dashboard sends one event of its own:
support:typing, while someone types in the support chat
(Live chat updates).
On connecting, the socket receives session: who it is and its token's
expires_at. The socket lasts as long as that token. To keep it open,
refresh the session (POST /merchant/auth/refresh) a minute before and send
the new token on the open socket:
await socket.emitWithAck("auth:refresh", newToken) answers
{"ok": true, "session": {…}}. Otherwise, at expires_at the server sends
auth:error with token_expired and disconnects (socket.io-client does not
reconnect after a server disconnect by itself): refresh, then connect again.
Signing out, a password change, a role change, removal from the team and a
suspension end the socket at once, with auth:error session_revoked,
role_changed, removed or suspended: sign in again. A refused handshake
is a connect_error unauthorized with err.data.code missing_token,
invalid_token or wrong_kind. A connection dropped for under 2 minutes is
recovered: the events missed are replayed, and session.recovered is true.
After a longer gap, load your screens again.