Vatly
Django

Receive signed webhooks

Mount the billing URLs and register the public HTTPS /billing/webhook/ URL in Vatly.

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.

ResponseMeaning
200 processedSignature accepted and transaction committed
200 duplicateSame signed ID was already committed
400Invalid signature, JSON or envelope
405Non-POST request
500Processing 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

ModelPurpose
BillingScopeTenant/storefront and mode; serializes callbacks with a database write lock
WebhookCallSigned envelope and processed timestamp, unique per scope/event
Checkout, ResourceLinkImmutable host-owned checkout/resource links
BillingResourceComplete financial or subscription snapshot and optional owner
OutboxDurable 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.

API reference · Admin

Copyright © 2026