E-commerce Flow
E-commerce Flow pattern for Python.
An end-to-end recipe for a typical e-commerce order: tokenize a card on the front-end, save it as a payment instrument, charge it once, and fulfill the order from a webhook.
Goal
You're selling discrete goods, not subscriptions. The flow you want is:
- Customer enters card details — tokenized client-side via Basis Theory
(the
client.basis_theory_public_api_keyis what your front-end uses). - Your server creates a
Customer, attaches the card as aPaymentInstrument, and charges the cart in one shot viaclient.checkout.create(...). - The
payment.createdwebhook fires; your handler marks the order paid and triggers fulfillment.
Implementation
1. Server: tokenize → checkout in one call
import os
import uuid
from easylabs import Client, EasyError, RateLimitError
client = Client(api_key=os.environ["EASY_API_KEY"])
def place_order(*, user, cart, card_token):
"""Charge the user once for `cart`. Idempotent on cart.id."""
try:
result = client.checkout.create(
customer={
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email,
},
payment_instrument={
"type": "PAYMENT_CARD",
"name": user.full_name,
"token_id": card_token, # from Basis Theory in the browser
},
items=[
{"price_id": item.price_id, "quantity": item.quantity}
for item in cart.items
],
idempotency_key=f"order-{cart.id}",
)
except RateLimitError as e:
raise RetryAfter(e.retry_after_seconds or 5)
except EasyError as e:
raise OrderFailed(e.code, e.message, e.details)
return {
"order_id": result.order_id,
"transfer_id": result.transfer.id if result.transfer else None,
}2. Webhook handler: mark paid, fulfill
from easylabs import Webhooks, InvalidRequestError
SECRET = os.environ["EASY_WEBHOOK_SECRET"]
def receive_webhook(request):
try:
event = Webhooks.construct_event(
payload=request.body,
signature=request.headers.get("x-easy-webhook-signature", ""),
secret=SECRET,
)
except InvalidRequestError:
return ("invalid", 400)
# Idempotent: skip if we've already processed this event.
if WebhookSeen.exists(event.id):
return ("", 204)
WebhookSeen.record(event.id)
if event.type == "payment.created":
transfer = event.data
order_id = (transfer.get("tags") or {}).get("order_id")
if order_id:
mark_order_paid(order_id, transfer["id"])
enqueue_fulfillment(order_id)
return ("", 204)3. Refund path
def refund_order(order):
return client.transfers.create_refund(
order.transfer_id,
refund_amount=order.total_cents,
tags={"reason": "customer_request"},
idempotency_key=f"refund-{order.id}-full",
)Tradeoffs
- Idempotency keys are non-negotiable. Without them, a retry on a flaky network can charge the customer twice. The keys above are deterministic per cart / order.
- Webhooks must be idempotent on
event.id. Easy Labs retries on non-2xx responses; the same event will arrive more than once if your handler is slow or the network blips. - Don't poll. If you find yourself calling
client.transfers.retrieve(...)in a loop, you're papering over webhook plumbing that's failing somewhere. - For two-step "auth then capture" flows (e.g. ship-then-charge),
swap
client.checkout.create(...)for an authorization-only checkout and useclient.authorizations.