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.succeeded and order.failed (see Webhooks).
  • For sandbox: pick a test card token like card_visa. For production: a real billing_id from EPD Gateway.
  • A product to charge against — orders charge against products, not free-form amounts. Create one with POST /v1/products and save its id as $PRODUCT_ID.

Steps

Create the customer (or look up an existing one)

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"
  }'
Attach the payment method

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 }'
Create the order

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" }
  }'
Confirm asynchronously via webhook

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

FailureWhat you’ll seeBest response
Card declinederror.code = card_declinedShow the card was declined; let the user try a different card.
Insufficient fundserror.code = insufficient_fundsSame as above; some users will retry on payday.
Expired carderror.code = expired_cardPrompt for a new card.
Gateway transient errorerror.code = gateway_error (5xx)Retry with the same X-EPD-Idempotency-Key.
Network timeout (no response)UnknownRetry with the same idempotency key — EPD dedupes.
billing_id from wrong environmenterror.code = invalid_vault_id or environment_mismatchMake sure live keys see live vault ids and sandbox sees sandbox.