One-time Payment
End-to-end recipe: collect a card, charge it once, confirm via webhook.
What you’ll build
A flow that takes a customer’s card details (in sandbox: a test token), creates an EPD customer, attaches the card, and runs a single charge. The charge result is delivered both synchronously in the response and asynchronously via webhook.
Use this recipe for: one-off purchases, top-ups, donations, services billed only once.
Prerequisites
- A sandbox API key (see Quickstart).
- A webhook endpoint registered for
order.succeededandorder.failed(see Webhooks). - For sandbox: pick a test card token like
card_visa. For production: a realbilling_idfrom EPD Gateway. - A product to charge against — orders charge against products, not free-form amounts. Create one with
POST /v1/productsand save itsidas$PRODUCT_ID.
Steps
EPD requires a first name, last name, and a phone number on every customer (in international format with a leading + and country code, e.g. +14155551234). Email is required too.
curl https://api.epd.com/v1/customers \
-H "Authorization: Bearer $EPD_KEY" \
-H "epd-version: 2026-02-11" \
-H "X-EPD-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+14155551234"
}'
Pass the gateway vault id (in sandbox: a card_* test token) as billing_id.
curl https://api.epd.com/v1/customers/$CUSTOMER_ID/payment_methods \
-H "Authorization: Bearer $EPD_KEY" \
-H "epd-version: 2026-02-11" \
-H "X-EPD-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "billing_id": "card_visa", "set_as_default": true }'
EPD creates the order and runs the charge in a single call. The response tells you whether the charge succeeded.
curl https://api.epd.com/v1/orders \
-H "Authorization: Bearer $EPD_KEY" \
-H "epd-version: 2026-02-11" \
-H "X-EPD-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "550e8400-e29b-41d4-a716-446655440000",
"payment_method_id": "6ba7b815-9dad-11d1-80b4-00c04fd430c8",
"items": [
{ "product_id": "'"$PRODUCT_ID"'", "quantity": 1 }
],
"metadata": { "internal_order_id": "ORD-1234" }
}'
Listen for order.succeeded (or order.failed) on your webhook endpoint and update your own database. The webhook is the source of truth — it survives client-side disconnects and double-submits.
Do not mark the user-facing order as paid based on the synchronous API response alone. A network drop after EPD captured the charge but before your code saw the response would leave you out of sync. Always reconcile against the webhook.
What can go wrong
| Failure | What you’ll see | Best response |
|---|---|---|
| Card declined | error.code = card_declined | Show the card was declined; let the user try a different card. |
| Insufficient funds | error.code = insufficient_funds | Same as above; some users will retry on payday. |
| Expired card | error.code = expired_card | Prompt for a new card. |
| Gateway transient error | error.code = gateway_error (5xx) | Retry with the same X-EPD-Idempotency-Key. |
| Network timeout (no response) | Unknown | Retry with the same idempotency key — EPD dedupes. |
billing_id from wrong environment | error.code = invalid_vault_id or environment_mismatch | Make sure live keys see live vault ids and sandbox sees sandbox. |