Webhooks¶
zeroth.service.webhooks ¶
Webhook delivery system for Zeroth platform.
Provides webhook subscription management, delivery lifecycle tracking, HMAC-SHA256 payload signing, and dead-letter handling.
DeliveryStatus ¶
Bases: StrEnum
Lifecycle states of a webhook delivery attempt.
EscalationAction ¶
Bases: StrEnum
Actions to take when an approval SLA expires.
WebhookDeadLetter ¶
Bases: BaseModel
A delivery that has been moved to the dead-letter queue.
Preserves all delivery metadata for later inspection or manual retry.
WebhookDelivery ¶
Bases: BaseModel
Tracks a single webhook delivery attempt.
Created when an event fires. A worker claims it (PENDING/FAILED -> DELIVERING) then resolves it: DELIVERING -> DELIVERED on success, or DELIVERING -> FAILED -> ... -> DEAD_LETTER once retries are exhausted.
WebhookEventPayload ¶
Bases: BaseModel
Standard envelope for webhook event data.
Wraps the event-specific data with common metadata fields for consistent parsing on the receiving end.
WebhookEventType ¶
Bases: StrEnum
Types of events that can trigger webhook deliveries.
WebhookSubscription ¶
Bases: BaseModel
A registered webhook endpoint for a specific deployment.
Tracks which event types should be delivered to which URL, along with the shared secret used for HMAC signing.
WebhookRepository ¶
WebhookRepository(
database: AsyncDatabase,
scope_context: ScopeContext | NullWorkspaceScopeContext,
)
Saves and loads webhook subscriptions, deliveries, and dead-letter entries.
Provides full CRUD for subscriptions, delivery lifecycle management (enqueue, claim, mark delivered/failed, dead-letter), and dead-letter queries.
create_subscription
async
¶
create_subscription(
sub: WebhookSubscription,
*,
audit_record: NodeAuditRecord | None = None,
audit_repository: AuditRepository | None = None,
) -> WebhookSubscription
Persist a new webhook subscription and return it.
get_subscription
async
¶
Look up a subscription by ID. Returns None if not found.
list_subscriptions
async
¶
Return subscriptions, optionally filtered by deployment and/or tenant.
list_subscriptions_for_event
async
¶
list_subscriptions_for_event(
deployment_ref: str, event_type: WebhookEventType
) -> list[WebhookSubscription]
Return active subscriptions for a deployment matching the given event type.
deactivate_subscription
async
¶
deactivate_subscription(
subscription_id: str,
*,
audit_record: NodeAuditRecord | None = None,
audit_repository: AuditRepository | None = None,
) -> None
Set a subscription to inactive.
delete_subscription
async
¶
Hard-delete a subscription.
enqueue_delivery
async
¶
Persist a new delivery with PENDING status.
enqueue_deliveries
async
¶
enqueue_deliveries(
deliveries: Sequence[WebhookDelivery],
*,
audit_records: Sequence[NodeAuditRecord] | None = None,
audit_repository: AuditRepository | None = None,
) -> list[WebhookDelivery]
Persist one source event's fan-out and audits under one commit.
get_delivery
async
¶
Return one delivery by ID for delivery verification and inspection.
list_deliveries
async
¶
list_deliveries(
subscription_id: str | None = None,
limit: int = 50,
subscription_ids: Sequence[str] | None = None,
) -> list[WebhookDelivery]
Return safe delivery records after applying subscription scope in the query.
claim_pending_delivery
async
¶
Claim the oldest delivery that is due for an attempt.
Atomically leases the delivery so the polling worker cannot
double-claim (and thus double-deliver) it: within one transaction
the chosen row is flipped to DELIVERING and its
next_attempt_at is pushed lease_seconds into the future, so a
subsequent claim won't see it until the lease lapses. The
The compare-and-swap predicate includes all observed lease state, so
concurrent transactions cannot both claim the same generation.
A claim covers three due cases:
PENDING-- reached its first-attempt time;FAILED-- its retry backoff has elapsed (the actual retry path);DELIVERING-- its lease expired, i.e. a worker died mid-delivery.
Returns the claimed delivery and opaque generation, or None.
mark_delivered
async
¶
mark_delivered(
delivery_id: str,
generation: int,
*,
audit_record: NodeAuditRecord | None = None,
audit_repository: AuditRepository | None = None,
) -> bool
Complete only the currently leased generation.
mark_failed
async
¶
mark_failed(
delivery_id: str,
generation: int,
*,
error: str,
status_code: int | None,
retry_delay: float,
audit_record: NodeAuditRecord | None = None,
audit_repository: AuditRepository | None = None,
) -> bool
Mark a delivery as failed and schedule the next retry.
The claim already incremented attempt_count; this fenced transition
schedules the next retry without allowing an older worker to overwrite it.
retry_delay is the final, already-jittered backoff in seconds:
the delivery worker owns the backoff policy (see next_retry_delay)
and this method persists it verbatim rather than re-deriving it.
dead_letter
async
¶
dead_letter(
delivery_id: str,
generation: int,
*,
dead_letter_id: str | None = None,
audit_record: NodeAuditRecord | None = None,
audit_repository: AuditRepository | None = None,
) -> str | None
Move a delivery to the dead-letter table.
Inserts into webhook_dead_letters from delivery data, then updates the delivery status to DEAD_LETTER. Returns the durable dead-letter ID only when this generation wins the fenced transition.
list_dead_letters
async
¶
list_dead_letters(
subscription_id: str | None = None,
limit: int = 50,
subscription_ids: Sequence[str] | None = None,
) -> list[WebhookDeadLetter]
Return dead-letter entries, optionally filtered by subscription.
subscription_ids restricts the query to a set of subscriptions so the
LIMIT is applied AFTER the tenant scope (audit F8 re-audit) — filtering in
Python after a global LIMIT would silently hide a deployment's own rows
behind newer foreign ones.
get_dead_letter
async
¶
Look up a single dead-letter entry by ID.
WebhookDeliveryWorker
dataclass
¶
WebhookDeliveryWorker(
repository: WebhookRepository,
http_client: AsyncClient,
audit_recorder: ServiceAuditRecorder | None = None,
poll_interval: float = 2.0,
max_concurrency: int = 16,
retry_base_delay: float = 1.0,
retry_max_delay: float = 300.0,
)
Background worker that polls for pending deliveries and sends HTTP POST requests.
poll_loop
async
¶
Continuously claim and deliver pending webhooks until cancelled.
WebhookService
dataclass
¶
WebhookService(
repository: WebhookRepository,
default_max_retries: int = 5,
audit_recorder: ServiceAuditRecorder | None = None,
)
High-level webhook operations: emit events, manage subscriptions, replay dead-letters.
emit_event
async
¶
emit_event(
*,
event_type: WebhookEventType | str,
deployment_ref: str,
tenant_id: str,
data: dict,
) -> list[WebhookDelivery]
Find active subscriptions matching deployment_ref + event_type, enqueue delivery each.
create_subscription
async
¶
create_subscription(
sub: WebhookSubscription,
*,
actor: ActorIdentity | None = None,
) -> WebhookSubscription
Persist a new webhook subscription.
get_subscription
async
¶
Look up a subscription by ID.
list_subscriptions
async
¶
List subscriptions, optionally filtered.
list_deliveries
async
¶
list_deliveries(
subscription_id: str | None = None,
limit: int = 50,
subscription_ids: Sequence[str] | None = None,
) -> list[WebhookDelivery]
List delivery state without exposing payloads or signing material.
deactivate_subscription
async
¶
Soft-delete a subscription by marking it inactive.
delete_subscription
async
¶
Hard-delete a subscription.
get_dead_letter
async
¶
Look up a single dead-letter entry by ID (for ownership scoping).
replay_dead_letter
async
¶
Re-enqueue a dead-letter entry as a new pending delivery.
list_dead_letters
async
¶
list_dead_letters(
subscription_id: str | None = None,
limit: int = 50,
subscription_ids: Sequence[str] | None = None,
) -> list[WebhookDeadLetter]
List dead-letter entries.
sign_payload ¶
Sign a payload with HMAC-SHA256 and return the hex digest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload_bytes
|
bytes
|
The raw bytes of the webhook payload to sign. |
required |
secret
|
str
|
The shared secret string for the subscription. |
required |
Returns:
| Type | Description |
|---|---|
str
|
A lowercase hex string of the HMAC-SHA256 signature. |