API reference · v3

The ezPayments API.

Create payment links, read transactions, and get signed webhooks when money moves. A REST API with JSON in and JSON out.

Base URL

https://app.ezpayments.co/api/v3/
  • Every path ends with a trailing slash, for example /api/v3/payment-links/. Always send it.
  • Send Content-Type: application/json. Responses are JSON.
  • The version is in the path. There is no version header.

Authentication

Authenticate every request with a secret key in the Authorization header:

Authorization: Bearer sk_test_…
KeyFormatUse
Secret, testsk_test_ + 64 hex charactersTest mode requests
Secret, livesk_live_ + 64 hex charactersLive mode requests
Publishablepk_test_… / pk_live_…Not used by the v3 API
  • A secret key is shown once, when it’s created. Store it in a secret manager.
  • Never use a secret key in a browser or mobile app. Call the API from your server.
  • A key has full access to your organization’s v3 resources. There are no per-key scopes.
  • A key can be locked to an IP allowlist. Requests from other addresses get 401.

Authentication errors return 401 with a detail message:

{ "detail": "Invalid or expired API key." }

Test and live mode

The key you use decides the mode. Test keys create test payment links that move no real money, and only see test links and test webhook endpoints. The mode is echoed in meta.mode and the X-EzPayments-Mode header.

Transactions and API keys are shared across modes: a test key can list live transactions and manage live keys. Treat test keys as carefully as live ones.

Responses and errors

Successful responses wrap the result in data, with request details in meta:

{
  "data": { "id": "62f1ac1b-…", "status": "active", … },
  "meta": { "request_id": "req_a0261129fc684ca6", "mode": "test" }
}

Every response carries an X-Request-Id header. Quote it when you contact support.

Errors use the HTTP status code and one of these bodies:

BodyWhen
{"error": {"type", "code", "message"}}Business rules, not found, permission and idempotency errors
{"field": ["message", …]}Request validation (400). All failing fields at once
{"detail": "message"}Authentication (401), unsupported method (405)
{"success": false, "code": "rate_limited", …}Rate limit (429)
StatusMeaning
200 / 201 / 204Success / created / deleted (no body)
400Invalid request
401Missing or invalid key, IP not allowed
403Your account can’t do this yet (for example, payment links need approval)
404Not found in your organization and mode
409Same Idempotency-Key still in progress
429Too many requests
5xxSomething went wrong on our side. Safe to retry with the same idempotency key
error.codeStatusMeaning
not_found404The resource doesn’t exist for your key
validation_error400A business rule failed, for example amount must be greater than zero
invalid_status400Only active payment links can be updated or cancelled
invalid_query_param400A filter value is wrong; param names it
invalid_url400Webhook URL must be HTTPS and public
self_revocation400You can’t revoke the key making the request
idempotency_key_too_long400Keep Idempotency-Key to 255 characters
request_in_flight409Retry after Retry-After seconds
feature_gate_not_approved403Payment links aren’t approved on your account yet; next_action says what to do
receiving_disabled, receiving_frozen, tenant_frozen, tenant_inactive403Your account can’t receive payments right now. Contact support
product_unavailable, gate_check_failed503Temporarily unavailable. Retry later

Pagination

List endpoints return newest first, using a cursor:

GET /api/v3/payment-links/?limit=20

{
  "data": {
    "next": "https://app.ezpayments.co/api/v3/payment-links/?limit=20&starting_after=cD0yMDI2…",
    "previous": null,
    "results": [ … ]
  },
  "meta": { … }
}
  • limit: 1 to 100, default 20.
  • To get the next page, request the next URL. starting_after is an opaque cursor, not an object id.
  • next is null on the last page. There is no total count.

Idempotency

Add an Idempotency-Key header to any POST so a retry never creates a second payment link, endpoint or key:

Idempotency-Key: order-1042-create-link
  • Up to 255 characters. Use a unique value per operation, such as your order id plus an action.
  • Keys are kept for at least 24 hours, per API key and path.
  • A repeated key returns the original response, even if the body is different. Never reuse a key for a different operation.
  • If the first request is still running you get 409 request_in_flight with Retry-After: 30.
  • Errors 401, 403, 429 and 5xx aren’t stored, so retrying runs the request again.

Rate limits

Each API key can make 60 requests per minute by default. Over the limit you get 429 with a Retry-After header in seconds:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

{ "success": false, "code": "rate_limited", "message": "Too many attempts. Please wait a moment and try again.", "data": { "retry_after_seconds": 60 } }

Back off for Retry-After seconds, then retry. Contact us if you need a higher limit.

A payment link is a hosted page where your customer pays a fixed amount in USD by card, Apple Pay, Google Pay, bank or ezPayments wallet. Links created through the API are single-use.

The payment link object

FieldTypeDescription
idstringUUID
amountstringUSD, 2 decimals, e.g. "75.00"
descriptionstringWhat the payment is for
customer_name, customer_emailstring"" if not set
reference_numberstringYour order or invoice number
urlstringThe payment page. Send this to your customer. Don’t build it yourself
statusstringactive, paid, expired or cancelled
expires_atstring or nullISO 8601. null never expires
paid_atstring or nullWhen it was paid
payment_method_typestringSet when paid, e.g. card, us_bank_account
total_platform_feesstringFees charged, set when paid
total_amount_with_feesstringWhat the customer pays (equals amount; fees come out of your side)
net_amount_to_companystringWhat you receive after fees
fee_breakdownobject or nullFee detail once paid
metadataobjectYour own key-value data
view_count, last_viewed_atinteger, stringPayment page views
tokenstringPublic token for the link
success_url, cancel_urlstringStored with the link
created_at, updated_atstringISO 8601, UTC

POST/api/v3/payment-links/

Create a payment link. Returns 201 and sends a payment_link.created webhook.

FieldRequiredRules
amountyes0.01 to 10,000.00, max 2 decimals. String or number
descriptionyes1 to 1,000 characters
customer_namenoUp to 255. Omit rather than send ""
customer_emailnoValid email
reference_numbernoUp to 100. Omit rather than send ""
expires_atnoISO 8601 with timezone, in the future
send_emailnotrue emails the link to customer_email
metadatanoUp to 50 keys, 4 KB in total
success_url, cancel_urlnohttp or https, up to 2,000 characters
curl https://app.ezpayments.co/api/v3/payment-links/ \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: order-1042-create-link" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "75.00",
    "description": "Order #1042",
    "customer_email": "jane@example.com",
    "reference_number": "ORD-1042",
    "send_email": true,
    "metadata": { "order_id": "1042" }
  }'
{
  "data": {
    "id": "62f1ac1b-fde0-4351-b004-00c2e89606a4",
    "amount": "75.00",
    "description": "Order #1042",
    "customer_email": "jane@example.com",
    "reference_number": "ORD-1042",
    "url": "https://app.ezpayments.co/pay/62f1ac1b-fde0-4351-b004-00c2e89606a4/",
    "status": "active",
    "expires_at": null,
    "paid_at": null,
    "total_amount_with_fees": "75.00",
    "net_amount_to_company": "75.00",
    "fee_breakdown": null,
    "metadata": { "order_id": "1042" },
    "created_at": "2026-10-27T21:30:46.705558Z",
    …
  },
  "meta": { "request_id": "req_a0261129fc684ca6", "mode": "test" }
}

GET/api/v3/payment-links/

List payment links. Filters: status, customer_email, reference_number, created_after, created_before (ISO 8601 with timezone), plus limit.

GET/api/v3/payment-links/{id}/

Retrieve one link. Use it to confirm a payment before you fulfil an order.

PATCH/api/v3/payment-links/{id}/

Update an active link. You can change description, customer_name, customer_email, reference_number, expires_at (null removes it), metadata (replaces the whole object), success_url and cancel_url. The amount can’t change.

DELETE/api/v3/payment-links/{id}/

Cancel an active link. Returns 200 with the link, now cancelled, and sends payment_link.cancelled.

GET/api/v3/payment-links/{id}/fees/

Preview the fee for this link’s amount for each payment method: domestic_card, intl_card, ach and stablecoin. Each has total_fee, net_amount, has_fee, fee_description and display_rate.

Transactions

Read-only access to your organization’s ledger: payments received, deposits, withdrawals, transfers, refunds and more.

GET/api/v3/transactions/

Filters: type, status, created_after, created_before, limit.

GET/api/v3/transactions/{id}/

FieldDescription
idUUID
transaction_typeguest_payment_link, invoice_payment, qr_payment, deposit, withdraw, transfer, batch_transfer, automatic_withdrawal, refund, chargeback, bill_payment, bill_settlement_payout
amount, fee_amount, net_amountStrings, USD
statuspending, success, failed, blocked, cancelled, incomplete, held, draft, awaiting_approval
stripe_payment_intent_id, stripe_transfer_id, stripe_payout_idProcessor references, or null
remarksDescription generated by ezPayments
created_at, updated_atISO 8601

A payment-link payment stays pending until the funds settle, then becomes success.

Webhook endpoints

POST/api/v3/webhook-endpoints/

Register a URL to receive events. The response includes the signing secret (whsec_…) once. Store it.

{
  "url": "https://example.com/webhooks/ezpayments",
  "description": "Order system",
  "events": ["payment_link.paid", "payment_link.expired", "payment_link.cancelled"]
}
  • url: HTTPS, publicly reachable, up to 500 characters.
  • events: at least one event type.
  • The endpoint takes the mode of the key that created it.

GET/api/v3/webhook-endpoints/ · GET/{id}/

List or retrieve endpoints. The secret is never returned again.

PATCH/api/v3/webhook-endpoints/{id}/

Change url, description, events or is_active (false pauses deliveries).

DELETE/api/v3/webhook-endpoints/{id}/

Remove an endpoint. Returns 204. To rotate a secret, create a new endpoint, switch over, then delete the old one.

API keys

POST/api/v3/api-keys/

Create a key with name and mode (test or live). The response includes secret_key once.

GET/api/v3/api-keys/

List keys with name, mode, prefix, is_active, last_used_at and rate_limit_per_minute. Secrets are never listed.

DELETE/api/v3/api-keys/{id}/

Revoke a key immediately. Returns 204. You can’t revoke the key making the request.

Webhooks

ezPayments sends an HTTPS POST to your endpoint when something happens.

EventSent whenBody
payment_link.createdA link is created through the APIPayment link object
payment_link.paidA card or bank payment on the link is recordedPayment link object, status: "paid"
payment_link.expiredAn active link passes expires_at (checked hourly){"id": "…"}
payment_link.cancelledA link is cancelled through the APIPayment link object

transaction.succeeded and transaction.failed are reserved and not sent yet.

What a delivery looks like

POST /webhooks/ezpayments
Content-Type: application/json
User-Agent: EzPayments-Webhooks/1.0
X-EzPayments-Event: payment_link.paid
X-EzPayments-Delivery: 9b2f5a10-3c1d-4e7a-8f60-2d4b1c9e7a55
X-EzPayments-Signature: t=1793136000,v1=5f0c…

{"amount": "75.00", "id": "62f1ac1b-…", "metadata": {"order_id": "1042"}, "paid_at": "…", "status": "paid", …}
  • The event type is in the X-EzPayments-Event header. The body is the object itself, with no wrapper.
  • X-EzPayments-Delivery stays the same across retries. Use it to ignore duplicates.
  • In webhook bodies all amounts are strings, including inside fee_breakdown.
  • Respond with any 2xx within 10 seconds, then do the work in the background.

Verify the signature

Compute HMAC-SHA256 of "{t}.{raw body}" using the whole secret (including whsec_) as the key, and compare it to v1 in constant time. Reject timestamps more than 5 minutes old. Always use the raw body bytes, not re-serialized JSON.

// Node.js (Express)
const crypto = require("crypto");

app.post("/webhooks/ezpayments", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("X-EzPayments-Signature") || "";
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const raw = req.body.toString("utf8");
  if (!/^\d+$/.test(parts.t || "") || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
    return res.status(400).end();
  }
  const expected = crypto.createHmac("sha256", process.env.EZP_WEBHOOK_SECRET)
    .update(`${parts.t}.${raw}`).digest("hex");
  const ok = parts.v1 && expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  if (!ok) return res.status(400).end();

  const event = req.get("X-EzPayments-Event");      // "payment_link.paid"
  const delivery = req.get("X-EzPayments-Delivery"); // dedupe on this
  const link = JSON.parse(raw);
  // queue your work, then:
  res.status(200).end();
});
# Python (Flask)
import hashlib, hmac, json, os, time

@app.post("/webhooks/ezpayments")
def ezpayments_webhook():
    raw = request.get_data()
    parts = dict(p.split("=", 1) for p in request.headers.get("X-EzPayments-Signature", "").split(",") if "=" in p)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return "", 400
    expected = hmac.new(os.environ["EZP_WEBHOOK_SECRET"].encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, v1):
        return "", 400
    event = request.headers["X-EzPayments-Event"]
    delivery = request.headers["X-EzPayments-Delivery"]
    link = json.loads(raw)
    return "", 200

Retries

A delivery that doesn’t get a 2xx within 10 seconds is retried, up to 5 attempts in total:

AttemptWhen
1Right away
2About 1 to 5 minutes later
3About 5 minutes after that
4About 30 minutes after that
5About 2 hours after that, then the delivery is marked failed

Deliveries can arrive more than once and out of order. Make your handler idempotent, and when it matters, confirm the current state with GET /api/v3/payment-links/{id}/.

Limits

LimitValue
Payment link amount0.01 to 10,000.00 USD
Description1,000 characters
Reference number100 characters
Metadata50 keys, 4 KB
Page sizeDefault 20, max 100
Rate limit60 requests per minute per key
Idempotency-Key255 characters, kept 24 hours or more
Webhook URLHTTPS, public, 500 characters
Webhook response time10 seconds
Using an AI coding assistant? Give it the AI agent guide: one copy-paste prompt with everything on this page.