Start a checkout
Configure a book offer in settings, then POST to the built-in
checkout route as an authenticated user. The view accepts an idempotency key, not
buyer-controlled products or prices.
Prepare one key per purchase
from uuid import uuid4
def example():
return {"purchase_key": str(uuid4())}
Persist the key with the host's purchase intent and pass it to your template. Re-render/retry with the same key after an ambiguous failure; do not generate a new UUID just because the API timed out. A genuinely new purchase needs a new key.
<form method="post" action="{% url 'vatly_django:checkout' offer='book' %}">
{% csrf_token %}
<input type="hidden" name="idempotency_key" value="{{ purchase_key }}">
<button type="submit">Buy the book</button>
</form>
Call from your own POST view
Pass a saved, authorized billable and the key you persisted:
from vatly_django.billing import Billing
def example(billable, purchase_key):
return Billing().checkout(billable, "book", idempotency_key=purchase_key)
Redirect to the returned hosted URL. A durable CheckoutAttempt records the entity, payload fingerprint and key before network I/O. Reusing the key for a different entity or changed offer fails. Idempotency retention is finite; investigate stale attempts rather than blindly retrying forever.
| Built-in view result | Meaning |
|---|---|
| 302 | Redirect to hosted checkout, or login for an unauthenticated request |
| 400 | Invalid key or offer |
| 403 | CSRF rejection |
| 405 | Non-POST request |
| 502 | SDK failure; retry/reconcile using the same purchase key |
A return-page visit is not proof of payment. Signed webhooks update local records; reconciliation handles missing snapshots. Customer identity alone never assigns a purchase to a billable.