One payments API for pay-ins, pay-outs and USDT
Create a pay-in, show the buyer how to pay, and get a signed webhook when it completes. The same REST API sends pay-outs and withdraws your USDT, and the sandbox runs the same contracts as production.
What you can build with the ATLAS API
Every merchant endpoint lives under https://api.atlas-online.io/api/merchant. Requests and responses are JSON over HTTPS (TLS 1.2 or newer), identifiers are UUIDs and timestamps are ISO 8601 in UTC.
The endpoints fall into four groups, and one set of API keys covers all of them.
-
Pay-ins:
POST /api/merchant/pay-increates the order and returns the payment details.GET /api/merchant/pay-in/{payInId}/statusreads it, andPOST /api/merchant/pay-in/{payInId}/utrpasses on the payer's transaction reference. -
Pay-outs:
POST /api/merchant/pay-outsends money to a bank account, a UPI ID or a Pago Móvil phone. Banks and payment methods per market come fromGET /api/merchant/banksandGET /api/merchant/payment-methods. -
Disputes:
POST /api/merchant/disputescontests a completed pay-in or pay-out, and the messages endpoint takes your evidence files. -
Finance:
GET /api/merchant/balanceshows your USDT position, andPOST /api/merchant/withdrawalssends USDT to your TRC-20 wallet.
Each request names its market in geo. The public reference covers Argentina (1, ARS), Venezuela (2, VES), India (3, INR) and South Korea (4, KRW), and INR and KRW amounts must be whole numbers. Turkey, Europe and the UK are live for pay-ins and pay-outs too. Ask us for their integration details. GET /api/merchant/payment-methods?geo={geo} returns the method codes your account can use, and the list of payment methods explains each one.
Three ways to integrate, one pay-in
All three paths start with the same signed call from your server, POST /api/merchant/pay-in. What changes is who draws the screen the buyer pays from.
Payment link
checkout.url checkout.url Embedded checkout
checkout.embedUrl Atlas.mount() H2H API
POST /api/merchant/pay-in checkout.url and checkout.embedUrl come back on the same pay-in, so you can redirect phone traffic and embed the page on desktop from one integration. On phones the docs lean toward the redirect, since fewer layers then sit between the buyer's tap and their banking app, and Atlas.shouldRedirect() gives you a hint.
Read the detail for each path: the payment link API, embedded checkout and the host to host payment API.
A sandbox that runs production contracts
You start with a sandbox dashboard and sandbox API keys. Endpoints, fields, statuses and webhooks match production, so moving to live traffic means switching the keys and the address. Markets, limits and terms are agreed with us before that switch.
The contract downloads from the docs as OpenAPI 3.1, in YAML for reading and JSON for code generators and validators. A Postman collection signs the HMAC headers for you, so your first request needs no code.
To see the buyer's side, open Pay-Ins → Test Pay Page in the sandbox merchant portal. It creates an ordinary pay-in with the same routing, statuses, expiry and webhooks as one created over the API.
-
Access
sandbox dashboard and API keys
-
Integrate
payment link, embedded page or H2H calls
-
Test
pay, confirm, webhook, return
-
Go live
production keys and address
HMAC signatures on every request
Each call to /api/merchant/* carries three headers: X-Api-Key, X-Timestamp (Unix seconds) and X-Signature. The signature is HMAC-SHA256 over the timestamp, a literal dot and the raw request body, keyed with your apiSecret and written as lowercase hex. A GET request signs an empty body.
The server compares signatures in constant time and refuses a timestamp more than 300 seconds away from its clock with 401 EXPIRED_REQUEST. The same apiSecret signs the webhooks we send you. When you rotate it through your account manager, the old secret keeps working for one hour.
The H2H API page walks through the exact-body rule, clock drift and every authentication error code.
const crypto = require('crypto');function sign(timestamp, body) { return crypto .createHmac('sha256', API_SECRET) .update(`${timestamp}.${body}`, 'utf8') .digest('hex');} Idempotency keys and safe retries
merchantOrderId is the idempotency key on pay-ins, pay-outs and disputes. If you send a pay-in again with the merchantOrderId of an order that has not failed, you get the original pay-in back with 201, including the same checkout.url.
So a timeout is safe to retry. On a 5xx or a dropped connection, retry with exponential backoff and the same merchantOrderId. Do not retry a 400, apart from NO_CARD_AVAILABLE, which can go again after a short delay. Accounts on strict idempotency get IDEMPOTENCY_KEY_MISMATCH when a known merchantOrderId arrives with a different body.
Withdrawals guard themselves another way. Only one can be in flight, and a second request gets WITHDRAWAL_IN_FLIGHT, so a duplicate can never pay out twice.
Signed webhooks and status tracking
ATLAS posts JSON to your webhookUrl when a transaction reaches a final state and when a dispute opens or closes. The events are pay_in.completed, pay_in.expired, pay_in.failed, pay_out.completed, pay_out.failed, pay_out.expired, dispute.opened and dispute.resolved.
Each delivery carries X-Webhook-Signature, an HMAC-SHA256 of the raw body keyed with your apiSecret. Verify it before you parse the JSON. Answer with any 2xx within 30 seconds. Failed deliveries retry after 5 seconds, 30 seconds, 2 minutes and 10 minutes, then hourly, up to 10 attempts over about five hours. The same event can arrive twice, so deduplicate on the event name plus payInId, payOutId or disputeId.
1 +5 s+30 s+2 min+10 min
+hourly 10
A pay-in starts as WaitingPayment and ends as Completed, Expired or Failed. An expired pay-in can still move to Completed when the payment is confirmed by hand afterwards. Pay-outs go through Pending, Claimed and PendingValidation before Completed, Expired or Failed, and one created before your balance is funded waits as AwaitingFunds for up to about 12 hours.
X-Webhook-Signature: <lowercase-hex HMAC-SHA256>{ "event": "pay_in.completed", "payInId": "1c75c312-93f3-4b34-903e-10fb38a56bec", "displayId": "PI00000007", "merchantOrderId": "order-12345", "status": "Completed", "geo": "India", "utr": "0086543219"} Pay-outs and finance endpoints
A pay-out takes the recipient fields of its rail and nothing else. In India that is an account number, bank and IFSC for IMPS, or a single recipientUpiVpa for UPI. Venezuela needs a phone, cédula, bank and account number, Argentina a CBU, and South Korea an account number and bank. A field from another rail is rejected with 400 VALIDATION_ERROR. Pay-outs draw on your USDT pay-out balance, and INSUFFICIENT_BALANCE tells you the shortfall. The pay-outs page covers every rail and its recipients.
GET /api/merchant/balance returns the same figures as the Finance screen in your dashboard: availableBalance from completed pay-ins, payOutBalance that backs pay-outs, reservedBalance held by pay-outs in flight, your TRC-20 walletAddress, and any pendingWithdrawal. When auto-settle is on for your account, a withdrawal of the whole pay-in balance is requested for you once it reaches accumulationThreshold. You can also call POST /api/merchant/withdrawals yourself. Withdrawal status is not pushed by webhook yet, so poll GET /api/merchant/withdrawals/{withdrawalId}. See USDT settlement for how USDT reaches your wallet.
Move USDT from your pay-in balance to your pay-out balance through the API with POST /api/merchant/transfers.
Contract versions and the changelog
Every response carries X-Api-Version, the date of the contract you are talking to. The changelog in the API docs lists each version.
New fields are appended, and nothing is renamed or reordered. Opt-in features such as checkout and includeUpiAppLinks leave the response byte-identical for callers that do not send them. Read keys by name and treat a missing key as "not applicable": a pay-in response only carries the payment details that fit its market and rail. Each release is listed in the changelog in the API docs with its migration notes.
The merchant overview covers methods, the pay page and settlement from the commercial side.
FAQ
Is there a sandbox for the ATLAS payments API?
Yes. You get a sandbox dashboard and API keys, and the sandbox runs the same contracts as production. The Test Pay Page in the portal creates sandbox pay-ins without any code.
How are API requests authenticated?
With a per-request HMAC-SHA256 signature. You send X-Api-Key, X-Timestamp and X-Signature, signed over the timestamp, a dot and the raw body, and requests more than 300 seconds off the server clock are rejected.
How do I avoid creating the same payment twice?
Give every order a unique merchantOrderId. Sending it again returns the original pay-in instead of a new one, so you can retry after a timeout.
Which events do the webhooks send?
pay_in.completed, pay_in.expired, pay_in.failed, pay_out.completed, pay_out.failed, pay_out.expired, dispute.opened and dispute.resolved. Each one is signed in X-Webhook-Signature.
What if my webhook endpoint is down?
Delivery gets up to 10 attempts over about five hours. You can also read any pay-in or pay-out from its status endpoint.
Is there an OpenAPI spec or a Postman collection?
Yes. The API docs offer OpenAPI 3.1 in YAML and JSON, and a Postman collection that signs requests for you.
Which markets does the API cover?
The reference documents Argentina, Venezuela, India and South Korea. The hosted and embedded pay page works in India and South Korea, and other markets connect through the API.
How do I withdraw my USDT?
Auto-settle sends it to your TRC-20 wallet. You can also withdraw on request, with POST /api/merchant/withdrawals or from the dashboard.