AtexPay API docs

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

StatusMeaning
401Missing or revoked key.
403verification_required — identity verification must be approved (and sales not paused) before this account can sell. Complete it in the AtexPay dashboard.
404The id does not exist, or belongs to another account.
409product_exists (the title is taken; product_id names the existing product) or not_deliverable (the order is not in progress).
422Validation failed; the body lists the fields.
429Rate limit hit — retry after a minute.
GET /v1/ping

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
curl https://api.atexpay.com/v1/ping \
  -H "Authorization: Bearer atx_YOUR_API_KEY"
Response · 200
{
  "data": {
    "ok": true,
    "seller": "Northline Studio",
    "time": "2026-09-05T12:00:00+00:00"
  }
}

Try it

GET /v1/products

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
curl https://api.atexpay.com/v1/products \
  -H "Authorization: Bearer atx_YOUR_API_KEY"
Response · 200
{
  "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

POST /v1/products

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.

FieldTypeRequiredDescription
namestringrequiredThe product title. 2–120 characters, shown in full at checkout. Unique per account.
descriptionstringrequiredWhat the buyer receives, shown in full at checkout. 10–1000 characters.
categorystringrequiredOne of design, development, writing, marketing, video_audio, education, ebooks, templates, software, photography, consulting, admin_support, other.
delivery_typestringrequireddownloadable or service. Shown to the buyer at checkout.
price_minorintegerrequiredPrice in minor units (cents). Minimum 100 ($1.00).
buyer_emailstringoptionalFor 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_urlstringoptionalAn 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
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"
  }'
Response · 201
{
  "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"
  }
}
Response · 409 — the title is taken
{
  "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

GET /v1/products/{id}

Retrieve a product

One product by its prod_ id. Returns 404 for ids that are not yours.

curl
curl https://api.atexpay.com/v1/products/prod_j2Nc81vWqTzY \
  -H "Authorization: Bearer atx_YOUR_API_KEY"

Try it

PATCH /v1/products/{id}

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
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

POST /v1/products/{id}/checkout-sessions

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.

FieldTypeRequiredDescription
emailstringoptionalThe buyer's email. Prefilled and hidden at checkout; must be a real, working mailbox — receipts and access go there.
namestringoptionalThe buyer's full name. Prefilled but editable — the payer may correct it.
addressobjectoptionalBilling address: country (2-letter code), line1, line2, city, state, postal_code. Prefilled but editable.
curl
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"
    }
  }'
Response · 201
{
  "data": {
    "url": "https://pay.atexpay.com/p/aB3xY9kQ2mNp?c=ZXlKcGRpSTZJbFE1…",
    "expires_at": "2026-09-06T09:00:00+00:00"
  }
}

Try it

GET /v1/orders

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
curl "https://api.atexpay.com/v1/orders?status=in_progress" \
  -H "Authorization: Bearer atx_YOUR_API_KEY"

Try it

GET /v1/orders/{id}

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
curl https://api.atexpay.com/v1/orders/ord_Q4mVn81xLpTe \
  -H "Authorization: Bearer atx_YOUR_API_KEY"
Response · 200 (timeline shortened)
{
  "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

POST /v1/orders/{id}/deliver

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
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"
Response · 200
{
  "data": { "id": "ord_Q4mVn81xLpTe", "status": "delivered", "deliverables": [ … ], "timeline": [ … ] }
}

Try it

GET /v1/transactions

List transactions

Your 100 most recent payments, newest first. Amounts are minor units; fee_minor is the single all-in transaction fee.

curl
curl https://api.atexpay.com/v1/transactions \
  -H "Authorization: Bearer atx_YOUR_API_KEY"
Response · 200
{
  "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

GET /v1/transactions/{id}

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
curl https://api.atexpay.com/v1/transactions/txn_p8Wm31kDzXvQ \
  -H "Authorization: Bearer atx_YOUR_API_KEY"
Response · 200
{
  "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:

  1. Register your endpoint URL in your dashboard under Settings → Developers — e.g. https://yoursite.com/webhooks/atexpay. One endpoint per account. The URL must be https and publicly reachable (localhost and private-network addresses are rejected). You get a signing secret (atxwh_…) — store it next to your API key.
  2. A buyer pays on your payment page, e.g. https://pay.atexpay.com/p/aB3xY9kQ2mNp.
  3. AtexPay POSTs a signed event to your URL — first payment.pending, then payment.succeeded once the charge is final (see Events & payload). The delivery looks like:
Example delivery to your endpoint
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":{…}}
  1. Your server verifies the signature with your atxwh_ secret (see Verifying signatures, or use the SDKs — one call) and answers any 2xx within 8 seconds. Acknowledge first, do your heavy work afterwards — anything else, or a timeout, counts as a failed delivery and is retried.
  2. You fulfil the order when a verified payment.succeeded arrives — 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.

POST your endpoint URL

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}.

EventWhen it fires
payment.pendingA payment started but has not completed yet (some bank methods take a while).
payment.succeededThe 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.failedThe payment definitively failed.
payment.refundedThe payment was refunded.
payment.disputedThe buyer opened a dispute with their bank.
pingThe "Send test event" button in your dashboard.
Event · payment.succeeded
{
  "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.

HeaderContents
webhook-idThe event id (evt_…) — stable across retries; your dedupe key.
webhook-timestampUnix seconds when this attempt was sent.
webhook-signaturev1,<base64 HMAC>
Node
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));
}
PHP
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.

PHP · PHP 8.1+, ext-curl
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']);
}
JavaScript · Node.js 18+, ESM
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']);
  }
});