# API reference | ezPayments

> The ezPayments v3 REST API: authentication, errors, pagination, idempotency, payment links, transactions, and signed webhooks.

Source: https://ezpayments.co/developers/api

[Overview](https://ezpayments.co/developers)[API reference](https://ezpayments.co/developers/api)

Basics

[Base URL](#base)[Authentication](#auth)[Test and live](#modes)[Responses and errors](#responses)[Pagination](#pagination)[Idempotency](#idempotency)[Rate limits](#ratelimits)

Resources

[Payment links](#payment-links)[Transactions](#transactions)[Webhook endpoints](#webhook-endpoints)[API keys](#api-keys)

Events

[Webhooks](#webhooks)[Verify signatures](#verify)[Retries](#retries)

More

[Limits](#limits)[Plugins](https://ezpayments.co/developers/plugins)[Build with AI agents](https://ezpayments.co/developers/ai-agents)

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.

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:

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

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

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

Using an AI coding assistant? Give it the [AI agent guide](https://ezpayments.co/developers/ai-agents): one copy-paste prompt with everything on this page.
