Requests and responses
These rules apply to every endpoint of the merchant API. Errors have their own page: Errors.
The basics
Section titled “The basics”| Base URL | https://api.ctpeway.com |
| Paths | Start with /v1, for example /v1/invoices. |
| Format | JSON in requests and responses. Send Content-Type: application/json with a body. |
| Authentication | Authorization: Bearer <your secret key>. See API keys. |
| Mode | Decided by the key: a test key works on test data, a live key on live data. |
| Amounts | Whole numbers of the asset’s smallest unit, as strings. See Networks and assets. |
| Names | Fields are written in snake_case. |
| Ids | 24 hexadecimal characters. Event ids are different: they start with evt_. |
Call the API from your server. It is not meant to be called from a web page.
Idempotency
Section titled “Idempotency”A request can fail in a way that leaves you unsure whether it worked, for example when the connection drops before the answer arrives. Sending it again could create a second invoice or, worse, a second payout.
An idempotency key removes that risk. Send a header with a value that is unique to the operation:
curl -X POST "https://api.ctpeway.com/v1/payouts" \ -H "Authorization: Bearer $CTPEWAY_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: payout-2026-10-13-supplier-7" \ -d '{ "asset": "USDT", "network": "tron", "amount": "250000000", "destination": "<destination-address>" }'- If you send the same key with the same request again, cTpeWay does not repeat the operation. It returns the answer it gave the first time, with the response header
Idempotent-Replayed: true. - If you send the same key with a different request, it is refused with status
422and the codeidempotency_key_reused. - If the first request with that key is still running, the answer is status
409and the codeidempotency_request_in_progress, with aRetry-Afterheader that says when to try again. - A key is remembered for 24 hours.
- A stored answer belongs to the API key that made the request. It is given only to that key, or to a key that has all of its permissions, such as the one that replaced it when you rolled it. Any other key gets status
403and the codeidempotency_key_not_yours. - Only
POSTrequests use it. Send it with everyPOST. Payouts and refunds, which move money, require it.
A good key comes from your own data, such as your order number or payout reference, so that a retry after a crash or a redeploy uses the same key.
The Node SDK sends an idempotency key with every POST by itself and reuses it when it retries. For calls that move money, pass your own: { idempotencyKey: 'payout-...' }. See Node SDK.
Pagination
Section titled “Pagination”Endpoints that return a list return one page at a time.
| Parameter | Meaning |
|---|---|
limit |
How many items to return, from 1 to 100. The default is 50. |
starting_after |
Return the items after this point. Pass the next_cursor of the previous page. |
A list response always has this shape:
{ "data": [], "has_more": true, "next_cursor": "..."}| Field | Meaning |
|---|---|
data |
The items of this page. |
has_more |
Whether there are more items after this page. |
next_cursor |
What to pass as starting_after to get the next page. It is null on the last page. |
To read everything, keep requesting with starting_after set to the last next_cursor until has_more is false.
# First pagecurl "https://api.ctpeway.com/v1/events?limit=50" \ -H "Authorization: Bearer $CTPEWAY_API_KEY"
# Next page: pass next_cursor from the previous answercurl "https://api.ctpeway.com/v1/events?limit=50&starting_after=<next_cursor>" \ -H "Authorization: Bearer $CTPEWAY_API_KEY"// One page.const page = await ctpeway.events.list({ limit: 50 });console.log(page.data.length, page.has_more, page.next_cursor);
// Every item, fetched page by page as the loop advances.for await (const event of ctpeway.events.list()) { console.log(event.id, event.type);}Filtering a list
Section titled “Filtering a list”Most list endpoints return the newest items first, and take filters as query parameters. Each endpoint’s reference page lists its own. The common ones:
| Filter | Example | Meaning |
|---|---|---|
| Status | ?status=paid |
Items in that status. Where the reference says so, you can give several, separated by commas: ?status=open,underpaid. |
| Network | ?network=bsc |
Items on that network, or on any of several separated by commas. |
| Time | ?created_after=2026-10-01T00:00:00Z |
Items created at or after that moment. created_before is the other end. Payments use detected_after and detected_before. |
| Text | ?q=order-1042 |
Items that contain the text, for example in an order id, a customer email or a transaction hash. |
Send the same filters with every page.
Request ids
Section titled “Request ids”Every response has a Request-Id header, for example req_.... The same value is in the request_id field of an error.
Log it. If you ask cTpeWay support about a request, the request id lets them find it at once.
Rate limits
Section titled “Rate limits”| Limit | Value |
|---|---|
| Requests per API key | 100 per second, with short bursts of up to 300. |
| Payouts created | 10 per hour for each account, by default. |
These limits come from the v2 design and can change before launch.
A request over the limit is answered with status 429 and an error of type rate_limited. Wait and try again, waiting longer after each failure. The Node SDK does this for you.
Versions and changes
Section titled “Versions and changes”The version is part of the path: /v1. Within a version cTpeWay only adds things. It does not remove or rename what is already there. Additions include new endpoints, new fields in responses, new optional request fields and new event types.
Write your code so that additions do not break it:
- ignore response fields you do not know;
- ignore webhook event types you do not know;
- do not assume that a list of possible values, such as statuses, will never grow.
Changes are listed in the changelog.

