Products & orders API
Create products and their checkout links from your own site, get a signed webhook the moment a sale changes status, and read your sales from your own systems. Every request is authenticated with a bearer key, all amounts are integer minor units (cents), and responses speak AtexPay identifiers only. The API creates products and reads data — payments run on your own seller account with our payment partner, the API never moves money, and every product it creates is screened and delivered exactly like one made in the dashboard. Prefer not to hand-roll requests? The drop-in PHP and JavaScript SDKs wrap every endpoint, including webhook verification.
Authentication
Create a key in your dashboard under Settings → Developers, then send it
on every request:
Header: Authorization: Bearer atx_…
Base URL: https://api.atexpay.com/v1
Paste your key below and every example on this page — including the try-it panels — uses it. The key is
kept only in this browser (localStorage), never sent anywhere except
api.atexpay.com, and you can clear it any time.
No key set — examples show atx_YOUR_API_KEY.
Conventions
Amounts are integers in minor units: 45000 means $450.00. Currency is
usd. Lists return your 100 most recent rows, newest first, under a
data key. Identifiers are opaque and stable: prod_… (products),
ord_… (orders), txn_… (payments), evt_… (webhook
events). Requests are limited to 60 per minute per key. Fees are per-transaction facts —
the all-in fee_minor exists only once a payment happens, so it appears on
transactions and webhook events, never on products. Prefilled checkout URLs carry an
opaque ?c= token — encrypted, bound to one product, and expiring after 24 hours.
Errors
| Status | Meaning |
|---|---|
401 | Missing or revoked key. |
403 | verification_required — identity verification must be approved (and sales not paused) before this account can sell. Complete it in the AtexPay dashboard. |
404 | The id does not exist, or belongs to another account. |
409 | product_exists (the title is taken; product_id names the existing product) or not_deliverable (the order is not in progress). |
422 | Validation failed; the body lists the fields. |
429 | Rate limit hit — retry after a minute. |
Ping
Confirms your key works and names the account it belongs to. Wire it into deploy checks and health monitors — it reads nothing else and changes nothing.
curl https://api.atexpay.com/v1/ping \
-H "Authorization: Bearer atx_YOUR_API_KEY"
{
"data": {
"ok": true,
"seller": "Northline Studio",
"time": "2026-09-05T12:00:00+00:00"
}
}
Try it
List products
Your 100 most recent products, newest first. Every product carries its own checkout_url — the page you send buyers to. Products carry no fee figures: the all-in fee is a per-sale fact, read it from the transaction once a payment lands.
curl https://api.atexpay.com/v1/products \
-H "Authorization: Bearer atx_YOUR_API_KEY"
{
"data": [
{
"id": "prod_j2Nc81vWqTzY",
"name": "Growth audit",
"slug": "growth-audit",
"description": "Six-week audit with a prioritised fix list, delivered as a PDF.",
"category": "marketing",
"delivery_type": "downloadable",
"price_type": "fixed",
"price_minor": 45000,
"currency": "usd",
"status": "active",
"checkout_url": "https://pay.atexpay.com/p/aB3xY9kQ2mNp",
"buyer_email": null,
"return_url": "https://yoursite.com/thanks",
"created_at": "2026-10-11T09:00:00+00:00",
"updated_at": "2026-10-11T09:00:00+00:00"
}
]
}
Try it
Create a product
Adds a product and returns it with its checkout_url, live at once. The product is screened against the prohibited products list first — a prohibited product is refused with a 422. Titles are unique per account: a title that matches an existing product (ignoring case and punctuation) is refused with 409 product_exists, carrying that product’s product_id so you can reuse or update it. Every sale opens an order for you to deliver.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | required | The product title. 2–120 characters, shown in full at checkout. Unique per account. |
description | string | required | What the buyer receives, shown in full at checkout. 10–1000 characters. |
category | string | required | One of design, development, writing, marketing, video_audio, education, ebooks, templates, software, photography, consulting, admin_support, other. |
delivery_type | string | required | downloadable or service. Shown to the buyer at checkout. |
price_minor | integer | required | Price in minor units (cents). Minimum 100 ($1.00). |
buyer_email | string | optional | For a custom order: prefills this customer’s email at checkout for every buyer of the product. To prefill one specific buyer, mint a prefilled checkout URL instead. |
return_url | string | optional | An https URL on your site. After paying, the buyer sees a brief confirmation and is sent there with ?status=success&product=prod_… appended. UX only — confirm payment with the webhook or the transaction endpoint, never the redirect. |
curl -X POST https://api.atexpay.com/v1/products \
-H "Authorization: Bearer atx_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Growth audit",
"description": "Six-week audit with a prioritised fix list, delivered as a PDF.",
"category": "marketing",
"delivery_type": "downloadable",
"price_minor": 45000,
"return_url": "https://yoursite.com/thanks"
}'
{
"data": {
"id": "prod_j2Nc81vWqTzY",
"name": "Growth audit",
"slug": "growth-audit",
"description": "Six-week audit with a prioritised fix list, delivered as a PDF.",
"category": "marketing",
"delivery_type": "downloadable",
"price_type": "fixed",
"price_minor": 45000,
"currency": "usd",
"status": "active",
"checkout_url": "https://pay.atexpay.com/p/aB3xY9kQ2mNp",
"buyer_email": null,
"return_url": "https://yoursite.com/thanks",
"created_at": "2026-10-11T09:00:00+00:00",
"updated_at": "2026-10-11T09:00:00+00:00"
}
}
{
"error": {
"type": "product_exists",
"message": "A product titled \"Growth audit\" already exists. Update it with PATCH /v1/products/prod_j2Nc81vWqTzY, or choose a different title.",
"product_id": "prod_j2Nc81vWqTzY"
}
}
Try it
Retrieve a product
One product by its prod_ id. Returns 404 for ids that are not yours.
curl https://api.atexpay.com/v1/products/prod_j2Nc81vWqTzY \
-H "Authorization: Bearer atx_YOUR_API_KEY"
Try it
Update a product
Updates a product’s details — send only the fields to update: name, description, category, delivery_type, price_minor, status (active or paused — paused products can’t be bought), buyer_email, return_url. Orders already placed keep exactly what was bought. Titles and descriptions are screened against the prohibited products list, and each title must be unique among your products (409 product_exists).
curl -X PATCH https://api.atexpay.com/v1/products/prod_j2Nc81vWqTzY \
-H "Authorization: Bearer atx_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "price_minor": 49000, "status": "active" }'
Try it
Prefill a checkout
Mints a checkout URL for one specific buyer with their details attached — use it when a
customer is already signed in on your site and shouldn't retype what you know. Their
email arrives prefilled and hidden (the payment page displays it instead); name and
address arrive prefilled but stay editable. The details travel as an opaque
?c= token: encrypted, bound to this product, and expiring after 24 hours —
nothing is readable in the URL and nothing can be altered. An expired or tampered token
simply renders the plain checkout. Pass at least one of email,
name or address.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | optional | The buyer's email. Prefilled and hidden at checkout; must be a real, working mailbox — receipts and access go there. |
name | string | optional | The buyer's full name. Prefilled but editable — the payer may correct it. |
address | object | optional | Billing address: country (2-letter code), line1, line2, city, state, postal_code. Prefilled but editable. |
curl -X POST https://api.atexpay.com/v1/products/prod_j2Nc81vWqTzY/checkout-sessions \
-H "Authorization: Bearer atx_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "client@company.com",
"name": "Jordan Buyer",
"address": {
"country": "US",
"line1": "123 Main St",
"city": "Any Town",
"state": "CA",
"postal_code": "12345"
}
}'
{
"data": {
"url": "https://pay.atexpay.com/p/aB3xY9kQ2mNp?c=ZXlKcGRpSTZJbFE1…",
"expires_at": "2026-09-06T09:00:00+00:00"
}
}
Try it
List orders
Your 100 most recent orders, newest first. Every paid sale opens one, in_progress until you deliver it; then delivered, and completed once the buyer downloads a file or opens the link. A full refund makes it canceled. Filter with ?status=in_progress (or delivered, completed, canceled) to find what still needs delivering.
curl "https://api.atexpay.com/v1/orders?status=in_progress" \
-H "Authorization: Bearer atx_YOUR_API_KEY"
Try it
Retrieve an order
One order by its ord_ id, with its deliverables and its timeline: payment, delivery, the delivery email, and every page visit, download and link open by the buyer, each with time, IP, browser and country — your record that the order was delivered. Delivered files are kept for 15 days (files_expire_at).
curl https://api.atexpay.com/v1/orders/ord_Q4mVn81xLpTe \
-H "Authorization: Bearer atx_YOUR_API_KEY"
{
"data": {
"id": "ord_Q4mVn81xLpTe",
"status": "delivered",
"product_id": "prod_j2Nc81vWqTzY",
"product": {
"name": "Growth audit",
"description": "Six-week audit with a prioritised fix list, delivered as a PDF.",
"category": "marketing",
"delivery_type": "downloadable"
},
"buyer": { "name": "Jordan Lee", "email": "client@company.com" },
"amount_minor": 45000,
"currency": "usd",
"transaction_id": "txn_p8Wm31kDzXvQ",
"delivery_note": "Your audit is attached.",
"deliverables": [
{ "kind": "file", "name": "growth-audit.pdf", "size_bytes": 482113, "available": true, "access_count": 1, "first_accessed_at": "2026-10-12T08:41:00+00:00" }
],
"files_expire_at": "2026-10-27T10:00:00+00:00",
"placed_at": "2026-10-11T14:03:00+00:00",
"delivered_at": "2026-10-12T10:00:00+00:00",
"completed_at": "2026-10-12T08:41:00+00:00",
"canceled_at": null,
"timeline": [
{ "type": "paid", "actor": "buyer", "at": "2026-10-11T14:03:00+00:00" },
{ "type": "delivered", "actor": "seller", "note": "Your audit is attached.", "at": "2026-10-12T10:00:00+00:00" },
{ "type": "file_downloaded", "actor": "buyer", "note": "growth-audit.pdf", "ip": "203.0.113.7", "country": "US", "at": "2026-10-12T08:41:00+00:00" }
]
}
}
Try it
Deliver an order
Delivers an in_progress order — the same delivery the dashboard makes. Send files as multipart/form-data (files[], up to 5, 20 MB each: PDF, ZIP, DOCX, DOC, XLSX, PPTX, TXT, CSV, PNG, JPG) and/or a delivery_url (Google Drive, Dropbox…), plus an optional delivery_note. At least one file or a link is required. The buyer is emailed a Get my deliverable link; the order completes when they collect it. Delivering an order twice is refused with 409 not_deliverable.
curl -X POST https://api.atexpay.com/v1/orders/ord_Q4mVn81xLpTe/deliver \
-H "Authorization: Bearer atx_YOUR_API_KEY" \
-F "delivery_note=Your audit is attached." \
-F "files[]=@growth-audit.pdf" \
-F "delivery_url=https://drive.google.com/drive/folders/abc"
{
"data": { "id": "ord_Q4mVn81xLpTe", "status": "delivered", "deliverables": [ … ], "timeline": [ … ] }
}
Try it
List transactions
Your 100 most recent payments, newest first. Amounts are minor units; fee_minor is the single all-in transaction fee.
curl https://api.atexpay.com/v1/transactions \
-H "Authorization: Bearer atx_YOUR_API_KEY"
{
"data": [
{
"id": "txn_p8Wm31kDzXvQ",
"status": "succeeded",
"currency": "usd",
"gross_minor": 45000,
"fee_minor": 2940,
"net_minor": 42060,
"buyer_email": "client@company.com",
"product": "Growth audit",
"product_id": "prod_j2Nc81vWqTzY",
"order_id": "ord_Q4mVn81xLpTe",
"occurred_at": "2026-08-26T14:03:00+00:00"
}
]
}
Try it
Retrieve a transaction
Everything about one payment, addressed by its txn_ id from the list endpoint: amounts, settlement state, payer, and payment method. Returns 404 for ids that are not yours.
curl https://api.atexpay.com/v1/transactions/txn_p8Wm31kDzXvQ \
-H "Authorization: Bearer atx_YOUR_API_KEY"
{
"data": {
"id": "txn_p8Wm31kDzXvQ",
"status": "succeeded",
"display_status": "settled",
"currency": "usd",
"gross_minor": 45000,
"tax_minor": 0,
"fee_minor": 2940,
"net_minor": 42060,
"refunded_minor": 0,
"payer": { "name": "Jordan Lee", "email": "client@company.com", "country": "US" },
"payment_method": { "type": "card", "card_brand": "visa", "card_last4": "4242" },
"product": "Growth audit",
"product_id": "prod_j2Nc81vWqTzY",
"order_id": "ord_Q4mVn81xLpTe",
"occurred_at": "2026-08-26T14:03:00+00:00",
"settled_at": "2026-08-28T14:03:00+00:00"
}
}
Try it
Webhooks — how they work
A webhook is how AtexPay tells your server that a payment changed status, the moment it
happens — no polling. You build one HTTPS endpoint on your own site (for example
https://yoursite.com/webhooks/atexpay) and AtexPay sends signed JSON events
to it. End to end:
- Register your endpoint URL in your dashboard under
Settings → Developers — e.g.
https://yoursite.com/webhooks/atexpay. One endpoint per account. The URL must behttpsand publicly reachable (localhost and private-network addresses are rejected). You get a signing secret (atxwh_…) — store it next to your API key. - A buyer pays on your payment page, e.g.
https://pay.atexpay.com/p/aB3xY9kQ2mNp. - AtexPay POSTs a signed event to your URL — first
payment.pending, thenpayment.succeededonce the charge is final (see Events & payload). The delivery looks like:
POST https://yoursite.com/webhooks/atexpay
content-type: application/json
user-agent: AtexPay-Webhooks/1.0
webhook-id: evt_8fJk2LmQp4Xr
webhook-timestamp: 1788690000
webhook-signature: v1,dGhpc2lzYW5leGFtcGxlc2lnbmF0dXJlLi4u
{"id":"evt_8fJk2LmQp4Xr","type":"payment.succeeded","created_at":"2026-09-05T12:00:00+00:00","data":{…}}
- Your server verifies the signature with your
atxwh_secret (see Verifying signatures, or use the SDKs — one call) and answers any2xxwithin 8 seconds. Acknowledge first, do your heavy work afterwards — anything else, or a timeout, counts as a failed delivery and is retried. - You fulfil the order when a verified
payment.succeededarrives — mark it paid, send the goods, grant access.
The webhook — or GET /v1/transactions/{id} — is the truth about a payment.
The checkout return_url redirect (the buyer landing back on, say,
https://yoursite.com/thanks?status=success) is for the buyer's experience
only; never fulfil an order because of it.
Housekeeping: updating the endpoint URL keeps your existing secret; rolling the secret
invalidates old signatures immediately; the Send test event button in
the dashboard fires a signed ping at your URL so you can prove the wiring
before real money moves.
Events & payload
One event per status transition — a redelivered processor event never produces a duplicate.
The data object is exactly the transaction shape returned by
GET /v1/transactions/{id}.
| Event | When it fires |
|---|---|
payment.pending | A payment started but has not completed yet (some bank methods take a while). |
payment.succeeded | The payment completed and its order (data.order_id) is open and waiting for delivery — the usual signal to deliver. Also re-emitted when a dispute resolves in your favour. |
payment.failed | The payment definitively failed. |
payment.refunded | The payment was refunded. |
payment.disputed | The buyer opened a dispute with their bank. |
ping | The "Send test event" button in your dashboard. |
{
"id": "evt_k2Rp84nWqYxM",
"type": "payment.succeeded",
"created_at": "2026-09-05T14:03:09+00:00",
"data": {
"id": "txn_p8Wm31kDzXvQ",
"status": "succeeded",
"display_status": "pending_settlement",
"currency": "usd",
"gross_minor": 45000,
"tax_minor": 0,
"fee_minor": 2940,
"net_minor": 42060,
"refunded_minor": 0,
"payer": { "name": "Jordan Lee", "email": "client@company.com", "country": "US" },
"payment_method": { "type": "card", "card_brand": "visa", "card_last4": "4242" },
"product": "Growth audit",
"product_id": "prod_j2Nc81vWqTzY",
"order_id": "ord_Q4mVn81xLpTe",
"occurred_at": "2026-09-05T14:03:00+00:00",
"settled_at": null
}
}
Verifying signatures
Every delivery is signed with your endpoint's secret (shown in Settings →
Developers). Verify before trusting a single byte: compute
HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw body} with the secret,
base64-encode it, and compare — constant-time — against the header. Reject timestamps more
than 5 minutes old, and always use the raw request body, not a re-serialisation.
| Header | Contents |
|---|---|
webhook-id | The event id (evt_…) — stable across retries; your dedupe key. |
webhook-timestamp | Unix seconds when this attempt was sent. |
webhook-signature | v1,<base64 HMAC> |
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyAtexPay(rawBody, headers, secret) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
const sig = headers["webhook-signature"];
if (!id || !ts || !sig) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = "v1," + createHmac("sha256", secret)
.update(`${id}.${ts}.${rawBody}`)
.digest("base64");
if (expected.length !== sig.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
function verifyAtexPay(string $rawBody, array $headers, string $secret): bool
{
$id = $headers["webhook-id"] ?? "";
$ts = $headers["webhook-timestamp"] ?? "";
$sig = $headers["webhook-signature"] ?? "";
if ($id === "" || $ts === "" || $sig === "") return false;
if (abs(time() - (int) $ts) > 300) return false;
$expected = "v1," . base64_encode(hash_hmac("sha256", "$id.$ts.$rawBody", $secret, true));
return hash_equals($expected, $sig);
}
Delivery & retries
The first attempt fires the instant we process the payment. If your endpoint answers with
anything other than 2xx — or takes longer than 8 seconds — we retry after
roughly 1, 5, 15 and 60 minutes (5 attempts in total). webhook-id stays the
same across retries, so process each id once and answer 2xx for ids you have
already seen.
Your dashboard shows the recent deliveries with their HTTP results, and the
Send test event button fires a signed ping so you can verify
your handler end-to-end before real money moves. Rolled your secret? Old signatures stop
verifying immediately. And if your endpoint was down past the last retry, the data is never
lost — poll GET /v1/transactions to catch up.
SDKs — PHP & JavaScript
Single-file, zero-dependency clients that wrap every endpoint on this page plus webhook
signature verification — an integration becomes one require or
import and a handful of method calls. Grab the file straight from the
official github.com/atexpay/sdk
repository (each SDK ships with a README and a worked example there). Both are
server-side libraries: your API key and webhook secret must never reach
a browser.
Get AtexPay.php Get atexpay.js browse the repo →
Every method maps to an endpoint above: ping(), createProduct(),
products(), product(id), updateProduct(),
createCheckoutSession(), orders(), order(id),
deliverOrder(), transactions() and transaction(id)
— plus two conveniences: createCheckoutUrl() uses the product with that title
(creating it the first time) and returns the URL to send the buyer to, prefilled when you
pass customer details; and webhook verification returns the parsed event or throws on a bad
signature. The SDKs are at 2.0.0.
require __DIR__.'/AtexPay.php';
$atexpay = new \AtexPay\Client(getenv('ATEXPAY_API_KEY'));
// Send a signed-in customer straight to a prefilled checkout for the
// product with this title (created the first time; email hidden):
$atexpay->redirectToCheckout([
'name' => 'Growth audit',
'description' => 'Six-week audit with a prioritised fix list, delivered as a PDF.',
'category' => 'marketing',
'delivery_type' => 'downloadable',
'price_minor' => 45000,
'return_url' => 'https://yoursite.com/thanks',
], [
'email' => $user->email,
'name' => $user->name,
]);
// Webhook endpoint — verify, respond fast, process after:
$event = \AtexPay\Webhook::verify(
file_get_contents('php://input'),
getallheaders(),
getenv('ATEXPAY_WEBHOOK_SECRET'),
);
if ($event['type'] === 'payment.succeeded') {
$atexpay->deliverOrder($event['data']['order_id'], [], ['/path/to/audit.pdf']);
}
import { AtexPay, verifyWebhook } from './atexpay.js';
const atexpay = new AtexPay(process.env.ATEXPAY_API_KEY);
// The product with this title (created the first time) → checkout URL:
const url = await atexpay.createCheckoutUrl(
{
name: 'Growth audit',
description: 'Six-week audit with a prioritised fix list, delivered as a PDF.',
category: 'marketing',
delivery_type: 'downloadable',
price_minor: 45000,
return_url: 'https://yoursite.com/thanks',
},
{ email: user.email, name: user.name },
);
res.redirect(url);
// Webhook endpoint (Express) — mount with a raw body parser:
app.post('/webhooks/atexpay', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try { event = verifyWebhook(req.body, req.headers, process.env.ATEXPAY_WEBHOOK_SECRET); }
catch { return res.sendStatus(400); }
res.sendStatus(200);
if (event.type === 'payment.succeeded') {
atexpay.deliverOrder(event.data.order_id, {}, ['/path/to/audit.pdf']);
}
});