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_…
| Key | Format | Use |
|---|---|---|
| Secret, test | sk_test_ + 64 hex characters | Test mode requests |
| Secret, live | sk_live_ + 64 hex characters | Live mode requests |
| Publishable | pk_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.
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:
| Body | When |
|---|---|
{"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) |
| Status | Meaning |
|---|---|
200 / 201 / 204 | Success / created / deleted (no body) |
400 | Invalid request |
401 | Missing or invalid key, IP not allowed |
403 | Your account can’t do this yet (for example, payment links need approval) |
404 | Not found in your organization and mode |
409 | Same Idempotency-Key still in progress |
429 | Too many requests |
5xx | Something went wrong on our side. Safe to retry with the same idempotency key |
| error.code | Status | Meaning |
|---|---|---|
not_found | 404 | The resource doesn’t exist for your key |
validation_error | 400 | A business rule failed, for example amount must be greater than zero |
invalid_status | 400 | Only active payment links can be updated or cancelled |
invalid_query_param | 400 | A filter value is wrong; param names it |
invalid_url | 400 | Webhook URL must be HTTPS and public |
self_revocation | 400 | You can’t revoke the key making the request |
idempotency_key_too_long | 400 | Keep Idempotency-Key to 255 characters |
request_in_flight | 409 | Retry after Retry-After seconds |
feature_gate_not_approved | 403 | Payment links aren’t approved on your account yet; next_action says what to do |
receiving_disabled, receiving_frozen, tenant_frozen, tenant_inactive | 403 | Your account can’t receive payments right now. Contact support |
product_unavailable, gate_check_failed | 503 | Temporarily 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
nextURL.starting_afteris an opaque cursor, not an object id. nextisnullon 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_flightwithRetry-After: 30. - Errors
401,403,429and5xxaren’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.
Payment links
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
| Field | Type | Description |
|---|---|---|
id | string | UUID |
amount | string | USD, 2 decimals, e.g. "75.00" |
description | string | What the payment is for |
customer_name, customer_email | string | "" if not set |
reference_number | string | Your order or invoice number |
url | string | The payment page. Send this to your customer. Don’t build it yourself |
status | string | active, paid, expired or cancelled |
expires_at | string or null | ISO 8601. null never expires |
paid_at | string or null | When it was paid |
payment_method_type | string | Set when paid, e.g. card, us_bank_account |
total_platform_fees | string | Fees charged, set when paid |
total_amount_with_fees | string | What the customer pays (equals amount; fees come out of your side) |
net_amount_to_company | string | What you receive after fees |
fee_breakdown | object or null | Fee detail once paid |
metadata | object | Your own key-value data |
view_count, last_viewed_at | integer, string | Payment page views |
token | string | Public token for the link |
success_url, cancel_url | string | Stored with the link |
created_at, updated_at | string | ISO 8601, UTC |
POST/api/v3/payment-links/
Create a payment link. Returns 201 and sends a payment_link.created webhook.
| Field | Required | Rules |
|---|---|---|
amount | yes | 0.01 to 10,000.00, max 2 decimals. String or number |
description | yes | 1 to 1,000 characters |
customer_name | no | Up to 255. Omit rather than send "" |
customer_email | no | Valid email |
reference_number | no | Up to 100. Omit rather than send "" |
expires_at | no | ISO 8601 with timezone, in the future |
send_email | no | true emails the link to customer_email |
metadata | no | Up to 50 keys, 4 KB in total |
success_url, cancel_url | no | http 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}/
| Field | Description |
|---|---|
id | UUID |
transaction_type | guest_payment_link, invoice_payment, qr_payment, deposit, withdraw, transfer, batch_transfer, automatic_withdrawal, refund, chargeback, bill_payment, bill_settlement_payout |
amount, fee_amount, net_amount | Strings, USD |
status | pending, success, failed, blocked, cancelled, incomplete, held, draft, awaiting_approval |
stripe_payment_intent_id, stripe_transfer_id, stripe_payout_id | Processor references, or null |
remarks | Description generated by ezPayments |
created_at, updated_at | ISO 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.
| Event | Sent when | Body |
|---|---|---|
payment_link.created | A link is created through the API | Payment link object |
payment_link.paid | A card or bank payment on the link is recorded | Payment link object, status: "paid" |
payment_link.expired | An active link passes expires_at (checked hourly) | {"id": "…"} |
payment_link.cancelled | A link is cancelled through the API | Payment 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-Eventheader. The body is the object itself, with no wrapper. X-EzPayments-Deliverystays the same across retries. Use it to ignore duplicates.- In webhook bodies all amounts are strings, including inside
fee_breakdown. - Respond with any
2xxwithin 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 "", 200Retries
A delivery that doesn’t get a 2xx within 10 seconds is retried, up to 5 attempts in total:
| Attempt | When |
|---|---|
| 1 | Right away |
| 2 | About 1 to 5 minutes later |
| 3 | About 5 minutes after that |
| 4 | About 30 minutes after that |
| 5 | About 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
| Limit | Value |
|---|---|
| Payment link amount | 0.01 to 10,000.00 USD |
| Description | 1,000 characters |
| Reference number | 100 characters |
| Metadata | 50 keys, 4 KB |
| Page size | Default 20, max 100 |
| Rate limit | 60 requests per minute per key |
| Idempotency-Key | 255 characters, kept 24 hours or more |
| Webhook URL | HTTPS, public, 500 characters |
| Webhook response time | 10 seconds |