# Build with AI agents | ezPayments

> Copy one prompt into your AI coding assistant and it adds ezPayments to your app: payment links, a verified webhook receiver, and tests.

Source: https://ezpayments.co/developers/ai-agents

Developers

[Overview](https://ezpayments.co/developers)[API reference](https://ezpayments.co/developers/api)[Webhooks](https://ezpayments.co/developers/api#webhooks)[Plugins](https://ezpayments.co/developers/plugins)[Build with AI agents](https://ezpayments.co/developers/ai-agents)[The prompt](#prompt)[How to use it](#how)[Before you go live](#check)

Build with AI agents

# Copy. Paste. Integrated.

Paste this prompt into your AI coding assistant (Claude Code, Cursor, Copilot, or any other) and it will add ezPayments to your app: payment links, a verified webhook receiver, and tests. It contains everything in the [API reference](https://ezpayments.co/developers/api), including the details assistants usually get wrong.

## The prompt

Full integration prompt

```
You are integrating ezPayments payments into this codebase. Follow these facts exactly; do not guess API details.

MY PROJECT
- Stack / framework: [fill in, e.g. Next.js API routes, Django, Laravel, Express]
- What I charge for: [fill in, e.g. one-off orders, invoices, bookings]

API BASICS
- Base URL: https://app.ezpayments.co/api/v3/  Every path ends with a trailing slash.
- Auth: header "Authorization: Bearer <secret key>". Test keys start sk_test_, live keys sk_live_. Read the key from the env var EZPAYMENTS_SECRET_KEY. Never expose it to a browser or mobile client; call the API only from the server.
- Send "Content-Type: application/json". Successful responses are { "data": ..., "meta": { "request_id", "mode" } }. Always read the object from "data".
- Errors: check the HTTP status. Bodies can be { "error": { "type", "code", "message" } }, a field map like { "amount": ["..."] } for 400 validation, { "detail": "..." } for 401, or { "success": false, "code": "rate_limited", ... } for 429. Handle all four shapes; log the X-Request-Id response header with every error.
- Rate limit: 60 requests per minute per key. On 429, wait the Retry-After header seconds, then retry.
- Idempotency: send a unique "Idempotency-Key" header on every POST (e.g. "order-<orderId>-create-link"). A repeated key returns the original response even if the body differs, so never reuse a key for a different operation. On 409 request_in_flight, wait Retry-After seconds. Retry 5xx and network errors with the SAME key.
- Amounts are USD strings with 2 decimals, e.g. "75.00". Never use floats for money; use decimal types or integer cents internally. Payment link amount must be 0.01 to 10000.00.
- IDs are UUIDs. Timestamps are ISO 8601 UTC.

CREATE A PAYMENT LINK
POST /api/v3/payment-links/
Body: { "amount": "75.00" (required), "description": "Order #1042" (required, max 1000 chars), "customer_email": "...", "customer_name": "...", "reference_number": "ORD-1042" (max 100), "expires_at": "2026-12-31T23:59:59Z" (optional, future, with timezone), "send_email": true|false, "metadata": { "order_id": "1042" } (max 50 keys, 4 KB) }
- Omit optional string fields instead of sending "" (customer_name and reference_number reject empty strings).
- Response 201: data.id, data.url (send the customer to this exact URL; never build it yourself), data.status = "active".
- Store data.id on my order record. Put my own order id in metadata.order_id.
- Do not set metadata.source to "shopify".
- 403 feature_gate_not_approved means payment links are not approved on the account yet; surface error.next_action to the developer, do not retry.

OTHER ENDPOINTS
- GET /api/v3/payment-links/{id}/ : current state. status is active, paid, expired or cancelled.
- GET /api/v3/payment-links/?status=&reference_number=&customer_email=&created_after=&created_before=&limit= : list, newest first. Paginate by requesting data.next until it is null. starting_after is an opaque cursor, not an id.
- PATCH /api/v3/payment-links/{id}/ : only while active; amount cannot change; metadata replaces the whole object.
- DELETE /api/v3/payment-links/{id}/ : cancels an active link (returns 200 with the link).
- GET /api/v3/transactions/?type=guest_payment_link&status=success : read-only ledger. A payment stays "pending" until funds settle.
- POST /api/v3/webhook-endpoints/ with { "url": "https://...", "events": ["payment_link.paid", "payment_link.expired", "payment_link.cancelled"] } returns data.secret (whsec_...) ONCE. Store it as EZPAYMENTS_WEBHOOK_SECRET. URL must be public HTTPS.

WEBHOOKS (the part integrations usually get wrong)
- ezPayments POSTs JSON to my endpoint with headers:
  X-EzPayments-Event: payment_link.paid | payment_link.expired | payment_link.cancelled | payment_link.created
  X-EzPayments-Delivery: <uuid, the same on every retry>
  X-EzPayments-Signature: t=<unix seconds>,v1=<hex>
- The EVENT TYPE IS ONLY IN THE X-EzPayments-Event HEADER. The body is the payment link object itself, with no { type, data } wrapper. For payment_link.expired the body is just { "id": "..." }.
- Verify before parsing: read the RAW request body bytes (disable JSON body parsing on this route). Split the header on "," and "=" to get t and v1. Reject if t is not an integer or |now - t| > 300 seconds. Compute hex(HMAC_SHA256(key = the full secret string including "whsec_", message = t + "." + raw body)). Compare to v1 with a constant-time comparison. Reject with 400 if anything fails.
- Respond 2xx within 10 seconds; do slow work in a background job.
- Deliveries can repeat and arrive out of order (up to 5 attempts over about 2.5 hours). Store processed X-EzPayments-Delivery ids and skip duplicates. Make order updates idempotent (paid stays paid).
- On payment_link.paid: find my order by body.id (or body.metadata.order_id), check body.amount matches the order, mark it paid, then fulfil.
- On payment_link.expired / payment_link.cancelled: mark the order unpaid/cancelled and release any reserved stock.
- payment_link.paid is not sent when a customer pays from their ezPayments wallet. As a safety net, if an order is still unpaid when the customer returns, or on a scheduled job, call GET /api/v3/payment-links/{id}/ and treat status "paid" as paid.
- Do not rely on transaction.succeeded / transaction.failed events; they are not sent yet.

TEST MODE
- Use an sk_test_ key and a test webhook endpoint while building. Test links move no real money. Pay with card 4242 4242 4242 4242, any future expiry, any CVC.
- Test and live webhook endpoints and secrets are separate. Keep separate env vars for each environment.

WHAT TO BUILD
1. A small ezPayments client module: base URL, auth header, JSON handling, Idempotency-Key on POST, retries with backoff on 429/5xx/network errors, and parsing of all four error shapes.
2. Checkout: when an order is placed, create a payment link (idempotency key from the order id), save the link id on the order, and redirect the customer to data.url.
3. A webhook route that verifies signatures exactly as described, dedupes on X-EzPayments-Delivery, and updates orders for paid, expired and cancelled.
4. A fallback that reads the payment link when the customer returns and on a schedule, for orders still unpaid.
5. A one-off script or admin action that registers the webhook endpoint and prints the secret once.
6. Tests: signature verification (valid, wrong secret, old timestamp, tampered body), duplicate delivery ignored, paid/expired/cancelled order transitions, and client error handling. Use recorded fixtures shaped exactly like the examples above.
7. Short README notes: env vars (EZPAYMENTS_SECRET_KEY, EZPAYMENTS_WEBHOOK_SECRET), how to switch test to live, how to run the tests.

Before writing code, list the files you will add or change. Match this codebase’s existing conventions. Do not invent endpoints or fields that are not listed here.
```

Webhook receiver only prompt

```
You are integrating ezPayments payments into this codebase. Follow these facts exactly; do not guess API details.

MY PROJECT
- Stack / framework: [fill in, e.g. Next.js API routes, Django, Laravel, Express]
- What I charge for: [fill in, e.g. one-off orders, invoices, bookings]

API BASICS
- Base URL: https://app.ezpayments.co/api/v3/  Every path ends with a trailing slash.
- Auth: header "Authorization: Bearer <secret key>". Test keys start sk_test_, live keys sk_live_. Read the key from the env var EZPAYMENTS_SECRET_KEY. Never expose it to a browser or mobile client; call the API only from the server.
- Send "Content-Type: application/json". Successful responses are { "data": ..., "meta": { "request_id", "mode" } }. Always read the object from "data".
- Errors: check the HTTP status. Bodies can be { "error": { "type", "code", "message" } }, a field map like { "amount": ["..."] } for 400 validation, { "detail": "..." } for 401, or { "success": false, "code": "rate_limited", ... } for 429. Handle all four shapes; log the X-Request-Id response header with every error.
- Rate limit: 60 requests per minute per key. On 429, wait the Retry-After header seconds, then retry.
- Idempotency: send a unique "Idempotency-Key" header on every POST (e.g. "order-<orderId>-create-link"). A repeated key returns the original response even if the body differs, so never reuse a key for a different operation. On 409 request_in_flight, wait Retry-After seconds. Retry 5xx and network errors with the SAME key.
- Amounts are USD strings with 2 decimals, e.g. "75.00". Never use floats for money; use decimal types or integer cents internally. Payment link amount must be 0.01 to 10000.00.
- IDs are UUIDs. Timestamps are ISO 8601 UTC.

CREATE A PAYMENT LINK
POST /api/v3/payment-links/
Body: { "amount": "75.00" (required), "description": "Order #1042" (required, max 1000 chars), "customer_email": "...", "customer_name": "...", "reference_number": "ORD-1042" (max 100), "expires_at": "2026-12-31T23:59:59Z" (optional, future, with timezone), "send_email": true|false, "metadata": { "order_id": "1042" } (max 50 keys, 4 KB) }
- Omit optional string fields instead of sending "" (customer_name and reference_number reject empty strings).
- Response 201: data.id, data.url (send the customer to this exact URL; never build it yourself), data.status = "active".
- Store data.id on my order record. Put my own order id in metadata.order_id.
- Do not set metadata.source to "shopify".
- 403 feature_gate_not_approved means payment links are not approved on the account yet; surface error.next_action to the developer, do not retry.

OTHER ENDPOINTS
- GET /api/v3/payment-links/{id}/ : current state. status is active, paid, expired or cancelled.
- GET /api/v3/payment-links/?status=&reference_number=&customer_email=&created_after=&created_before=&limit= : list, newest first. Paginate by requesting data.next until it is null. starting_after is an opaque cursor, not an id.
- PATCH /api/v3/payment-links/{id}/ : only while active; amount cannot change; metadata replaces the whole object.
- DELETE /api/v3/payment-links/{id}/ : cancels an active link (returns 200 with the link).
- GET /api/v3/transactions/?type=guest_payment_link&status=success : read-only ledger. A payment stays "pending" until funds settle.
- POST /api/v3/webhook-endpoints/ with { "url": "https://...", "events": ["payment_link.paid", "payment_link.expired", "payment_link.cancelled"] } returns data.secret (whsec_...) ONCE. Store it as EZPAYMENTS_WEBHOOK_SECRET. URL must be public HTTPS.

WEBHOOKS (the part integrations usually get wrong)
- ezPayments POSTs JSON to my endpoint with headers:
  X-EzPayments-Event: payment_link.paid | payment_link.expired | payment_link.cancelled | payment_link.created
  X-EzPayments-Delivery: <uuid, the same on every retry>
  X-EzPayments-Signature: t=<unix seconds>,v1=<hex>
- The EVENT TYPE IS ONLY IN THE X-EzPayments-Event HEADER. The body is the payment link object itself, with no { type, data } wrapper. For payment_link.expired the body is just { "id": "..." }.
- Verify before parsing: read the RAW request body bytes (disable JSON body parsing on this route). Split the header on "," and "=" to get t and v1. Reject if t is not an integer or |now - t| > 300 seconds. Compute hex(HMAC_SHA256(key = the full secret string including "whsec_", message = t + "." + raw body)). Compare to v1 with a constant-time comparison. Reject with 400 if anything fails.
- Respond 2xx within 10 seconds; do slow work in a background job.
- Deliveries can repeat and arrive out of order (up to 5 attempts over about 2.5 hours). Store processed X-EzPayments-Delivery ids and skip duplicates. Make order updates idempotent (paid stays paid).
- On payment_link.paid: find my order by body.id (or body.metadata.order_id), check body.amount matches the order, mark it paid, then fulfil.
- On payment_link.expired / payment_link.cancelled: mark the order unpaid/cancelled and release any reserved stock.
- payment_link.paid is not sent when a customer pays from their ezPayments wallet. As a safety net, if an order is still unpaid when the customer returns, or on a scheduled job, call GET /api/v3/payment-links/{id}/ and treat status "paid" as paid.
- Do not rely on transaction.succeeded / transaction.failed events; they are not sent yet.

TEST MODE
- Use an sk_test_ key and a test webhook endpoint while building. Test links move no real money. Pay with card 4242 4242 4242 4242, any future expiry, any CVC.
- Test and live webhook endpoints and secrets are separate. Keep separate env vars for each environment.

WHAT TO BUILD
Only the webhook receiver: a route that verifies signatures exactly as described, dedupes on X-EzPayments-Delivery, and updates my orders for payment_link.paid, payment_link.expired and payment_link.cancelled. Include tests for valid, wrong-secret, old-timestamp and tampered-body cases, and for duplicate deliveries. Do not invent endpoints or fields that are not listed here.
```

Payment links only prompt

```
You are integrating ezPayments payments into this codebase. Follow these facts exactly; do not guess API details.

MY PROJECT
- Stack / framework: [fill in, e.g. Next.js API routes, Django, Laravel, Express]
- What I charge for: [fill in, e.g. one-off orders, invoices, bookings]

API BASICS
- Base URL: https://app.ezpayments.co/api/v3/  Every path ends with a trailing slash.
- Auth: header "Authorization: Bearer <secret key>". Test keys start sk_test_, live keys sk_live_. Read the key from the env var EZPAYMENTS_SECRET_KEY. Never expose it to a browser or mobile client; call the API only from the server.
- Send "Content-Type: application/json". Successful responses are { "data": ..., "meta": { "request_id", "mode" } }. Always read the object from "data".
- Errors: check the HTTP status. Bodies can be { "error": { "type", "code", "message" } }, a field map like { "amount": ["..."] } for 400 validation, { "detail": "..." } for 401, or { "success": false, "code": "rate_limited", ... } for 429. Handle all four shapes; log the X-Request-Id response header with every error.
- Rate limit: 60 requests per minute per key. On 429, wait the Retry-After header seconds, then retry.
- Idempotency: send a unique "Idempotency-Key" header on every POST (e.g. "order-<orderId>-create-link"). A repeated key returns the original response even if the body differs, so never reuse a key for a different operation. On 409 request_in_flight, wait Retry-After seconds. Retry 5xx and network errors with the SAME key.
- Amounts are USD strings with 2 decimals, e.g. "75.00". Never use floats for money; use decimal types or integer cents internally. Payment link amount must be 0.01 to 10000.00.
- IDs are UUIDs. Timestamps are ISO 8601 UTC.

CREATE A PAYMENT LINK
POST /api/v3/payment-links/
Body: { "amount": "75.00" (required), "description": "Order #1042" (required, max 1000 chars), "customer_email": "...", "customer_name": "...", "reference_number": "ORD-1042" (max 100), "expires_at": "2026-12-31T23:59:59Z" (optional, future, with timezone), "send_email": true|false, "metadata": { "order_id": "1042" } (max 50 keys, 4 KB) }
- Omit optional string fields instead of sending "" (customer_name and reference_number reject empty strings).
- Response 201: data.id, data.url (send the customer to this exact URL; never build it yourself), data.status = "active".
- Store data.id on my order record. Put my own order id in metadata.order_id.
- Do not set metadata.source to "shopify".
- 403 feature_gate_not_approved means payment links are not approved on the account yet; surface error.next_action to the developer, do not retry.

OTHER ENDPOINTS
- GET /api/v3/payment-links/{id}/ : current state. status is active, paid, expired or cancelled.
- GET /api/v3/payment-links/?status=&reference_number=&customer_email=&created_after=&created_before=&limit= : list, newest first. Paginate by requesting data.next until it is null. starting_after is an opaque cursor, not an id.
- PATCH /api/v3/payment-links/{id}/ : only while active; amount cannot change; metadata replaces the whole object.
- DELETE /api/v3/payment-links/{id}/ : cancels an active link (returns 200 with the link).
- GET /api/v3/transactions/?type=guest_payment_link&status=success : read-only ledger. A payment stays "pending" until funds settle.
- POST /api/v3/webhook-endpoints/ with { "url": "https://...", "events": ["payment_link.paid", "payment_link.expired", "payment_link.cancelled"] } returns data.secret (whsec_...) ONCE. Store it as EZPAYMENTS_WEBHOOK_SECRET. URL must be public HTTPS.

WEBHOOKS (the part integrations usually get wrong)
- ezPayments POSTs JSON to my endpoint with headers:
  X-EzPayments-Event: payment_link.paid | payment_link.expired | payment_link.cancelled | payment_link.created
  X-EzPayments-Delivery: <uuid, the same on every retry>
  X-EzPayments-Signature: t=<unix seconds>,v1=<hex>
- The EVENT TYPE IS ONLY IN THE X-EzPayments-Event HEADER. The body is the payment link object itself, with no { type, data } wrapper. For payment_link.expired the body is just { "id": "..." }.
- Verify before parsing: read the RAW request body bytes (disable JSON body parsing on this route). Split the header on "," and "=" to get t and v1. Reject if t is not an integer or |now - t| > 300 seconds. Compute hex(HMAC_SHA256(key = the full secret string including "whsec_", message = t + "." + raw body)). Compare to v1 with a constant-time comparison. Reject with 400 if anything fails.
- Respond 2xx within 10 seconds; do slow work in a background job.
- Deliveries can repeat and arrive out of order (up to 5 attempts over about 2.5 hours). Store processed X-EzPayments-Delivery ids and skip duplicates. Make order updates idempotent (paid stays paid).
- On payment_link.paid: find my order by body.id (or body.metadata.order_id), check body.amount matches the order, mark it paid, then fulfil.
- On payment_link.expired / payment_link.cancelled: mark the order unpaid/cancelled and release any reserved stock.
- payment_link.paid is not sent when a customer pays from their ezPayments wallet. As a safety net, if an order is still unpaid when the customer returns, or on a scheduled job, call GET /api/v3/payment-links/{id}/ and treat status "paid" as paid.
- Do not rely on transaction.succeeded / transaction.failed events; they are not sent yet.

TEST MODE
- Use an sk_test_ key and a test webhook endpoint while building. Test links move no real money. Pay with card 4242 4242 4242 4242, any future expiry, any CVC.
- Test and live webhook endpoints and secrets are separate. Keep separate env vars for each environment.

WHAT TO BUILD
Only payment link creation: a server function that creates a payment link for an order with an Idempotency-Key, stores the link id, and returns data.url for redirect or for sending to the customer by email or chat. Add a status check that reads GET /api/v3/payment-links/{id}/. Include tests for success, validation errors, 429 retry and idempotent retry. Do not invent endpoints or fields that are not listed here.
```

## How to use it

1.  Create a **test** secret key (`sk_test_…`) in your ezPayments dashboard and keep it in an environment variable, never in code.
2.  Open your project in your AI assistant and paste the prompt. Fill in the two lines at the top about your stack and what you sell.
3.  Let it build, then run the tests it writes. Create a test payment link and pay it with the test card `4242 4242 4242 4242`, any future expiry, any CVC.
4.  Review the code like any other pull request, especially the webhook signature check.
5.  When everything works in test mode, create a live key and a live webhook endpoint.

## Before you go live

-   Secret keys only on your server, loaded from environment variables.
-   Webhook signatures verified on the raw body, with a 5-minute timestamp window.
-   Duplicate webhooks ignored using `X-EzPayments-Delivery`.
-   Orders fulfilled only after a verified `payment_link.paid`, or after you read the link and see `status: "paid"`.
-   Every `POST` sends an `Idempotency-Key`.
-   Live webhook endpoint registered with a live key, and its secret stored separately from the test one.
