API Documentation
Integrate your system with the Payment System API
Create invoices, share payment links and manage customers programmatically — or let an AI assistant do it through MCP. Both interfaces use the same validation, permissions and merchant scoping.
Choose the right API surface
- Merchant/admin API — authenticated tools for merchant accounts, invoices, customers, and catalog data.
- Public Shared Events REST API
Quick start
- Create an API token on the API Tokens page (Profile → API Tokens)
- Copy the base URL below
- Send your first request
curl https://admin.onepay.tabsystems.tech/api/v1/invoices \
-H "Authorization: Bearer psk_your_token_here"Authentication
Every request needs an Authorization header with your API token. Tokens can expire and can be revoked on the API Tokens page (Profile → API Tokens).
A token inherits the role of the user who created it and can be revoked or given an expiry on the profile page — revocation takes effect immediately for both REST and MCP.
Authorization: Bearer psk_your_token_hereErrors
| 400 | Validation failed: body/query does not match the schema; `issues` lists the Zod errors |
| 401 | Missing, invalid, expired, or revoked API token |
| 403 | Token lacks permission for this action |
| 422 | Domain rule rejected the request (e.g. invalidAmount, noPaymentProvider) |
Webhooks
Give an API token a webhook URL on the API Tokens page (Profile → API Tokens) and we POST an event to it whenever an invoice created with that token changes as below. The invoice carries the externalReference and metadata you sent when creating it (metadata is limited to 8 KB of UTF-8 JSON; larger is rejected with 400). Revoking the token does not stop events for invoices it already created.
| invoice.paid | The invoice was paid (captured). |
| invoice.expired | The invoice was marked expired. |
| invoice.cancelled | The invoice was cancelled. |
| webhook.test | Sent only when you press "Send test event" on the API Tokens page: data holds apiTokenId and a message, and there is no invoice. It is signed like every event and tried once, never retried. Answer 2xx; you may otherwise ignore it. |
{
"id": "evt_5f0c2b7a9d1e4c3b8a6f2e10",
"type": "invoice.paid",
"createdAt": "2026-10-06T03:15:07.120Z",
"data": {
"invoice": {
"id": "cmg1x2y3z0000abcd1234efgh",
"status": "captured",
"title": "Order 1042",
"amount": 110000,
"taxAmount": 10000,
"currency": "MNT",
"externalReference": "order-1042",
"metadata": { "cartId": "c-77" },
"paidAt": "2026-10-06T03:15:05.000Z",
"expiresAt": "2026-10-07T00:00:00.000Z",
"createdAt": "2026-10-06T02:58:41.000Z"
}
}
}Verifying the signature
Every request carries x-onepay-timestamp (Unix seconds) and x-onepay-signature: the hex HMAC-SHA256 of timestamp + "." + raw body, keyed with the webhook's whsec_ secret (never your API token). Recompute it over the raw body, compare in constant time, and reject old timestamps.
import { createHmac, timingSafeEqual } from 'node:crypto'
const TOLERANCE_SECONDS = 300
// rawBody: the request body exactly as received (a string), before any JSON parsing.
// secret: the whsec_… value shown when you set the webhook URL — not your API token.
export const verifyOnePayWebhook = (rawBody, headers, secret) => {
const timestamp = headers['x-onepay-timestamp']
const signature = headers['x-onepay-signature']
if (!timestamp || !signature) return false
// Refuse old deliveries so a captured request cannot be replayed later.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
return expected.length === signature.length
&& timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}Answer with any 2xx within 10 seconds; anything else (including redirects) is a failure, retried after 1 min, 5 min, 30 min, 2 h and 12 h, then given up. A failed delivery can be resent from the latest deliveries in the key's webhook dialog (Profile → API Tokens). An event can arrive more than once — deduplicate on its id (also in x-onepay-event-id).
Rotating a key copies its webhook URL, secret and enabled flag to the new key. Invoices keep the key that created them: those created with the old key go on using the old key's webhook settings as copied at rotation — editing the new key's webhook afterwards does not change them (the old key's webhook stays editable on the same page).
You can also manage a key's webhook with that key itself, without the admin UI: GET/PUT/DELETE /api/v1/webhook, POST /api/v1/webhook/secret (requests a rotation), POST /api/v1/webhook/test and GET /api/v1/webhook/deliveries (the latest 5, a connection check) — or the MCP tools get_webhook, set_webhook, rotate_webhook_secret, send_test_webhook, list_webhook_deliveries and delete_webhook. Each acts only on the webhook of the key that authenticates the request, applies the same https and public-address checks, and returns the secret only when set_webhook first creates it. A rotation requested this way is not applied: the key owner gets an email and confirms or rejects it on the API Tokens page within 30 minutes, and the new secret is shown only there — so a leaked key alone cannot swap the secret. Changes show on the API Tokens page at once.