Receive signed webhooks
Mount the billing URLs and register the public HTTPS
/billing/webhook/ URL in Vatly. Set VATLY_WEBHOOK_SECRET to that endpoint's
secret. Installing the package does not register it automatically.
Use the built-in receiver
The built-in route verifies raw request bytes, checks the key's mode and stores the signed event ID. No custom view is required. If you need a separate adapter, the facade exposes the same processor:
from vatly_django.billing import Billing
def example(raw_body: bytes, signature: str):
return Billing().webhook(raw_body, signature)
Pass the unmodified request body and Vatly-Signature header. Do not reconstruct
JSON or trust an unsigned Vatly-Event-Id header for deduplication.
| Response | Meaning |
|---|---|
200 processed | Signature accepted and transaction committed |
200 duplicate | Same signed ID was already committed |
| 400 | Invalid signature, JSON or envelope |
| 405 | Non-POST request |
| 500 | Processing failure; all writes roll back for a safe delivery retry |
Signature freshness defaults to ±300 seconds. Keep server clocks synchronized; configure a sequence of secrets for rotation. Only this webhook route is CSRF-exempt.
Know what is stored
| Model | Purpose |
|---|---|
BillingScope | Tenant/storefront and mode; serializes callbacks with a database write lock |
WebhookCall | Signed envelope and processed timestamp, unique per scope/event |
Checkout, ResourceLink | Immutable host-owned checkout/resource links |
BillingResource | Complete financial or subscription snapshot and optional owner |
Outbox | Durable events for your background worker |
Audit, dedup, links, snapshots and outbox commit in one transaction. The ORM driver uses thread-sensitive async adapters so all nested work stays on that connection. It assumes Django's default database; custom routers need a purpose-built driver. PostgreSQL supports concurrent workers; SQLite delivery lock failures can be retried.
Built-ins make no implicit API reads. Unknown events remain audited/dispatched. Customer bindings never fill missing ownership, and deduplication is not event ordering. Reconcile delayed/conflicting snapshots explicitly.
Deliver your outbox
from vatly_django.models import Outbox
def example():
return list(
Outbox.objects.filter(delivered_at__isnull=True)
.order_by("pk")
.values("pk", "event_name", "payload")[:100]
)
This is an inspection query, not a safe concurrent worker. Your worker must claim rows with database locks, send using an idempotent key (scope + row primary key), and mark delivered_at after success. External delivery is at least once. Created events can precede ownership linkage: read current ownership before side effects. No email sender or entitlement side effect is enabled by default.