Easy Labs
SDKsPythonExamples

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:

  1. Customer enters card details — tokenized client-side via Basis Theory (the client.basis_theory_public_api_key is what your front-end uses).
  2. Your server creates a Customer, attaches the card as a PaymentInstrument, and charges the cart in one shot via client.checkout.create(...).
  3. The payment.created webhook 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 use client.authorizations.

On this page