Status: Production partner preview for admitted personal tenants. Bearer management, forward task-event projection, and HMAC-signed outbound delivery are active.Praxa projects each new durable task-ledger event into a strict v1 webhook event, fans it out to matching active endpoints, and signs every delivery. Delivery is at least once and may arrive out of order. The signed-in developer portal manages endpoints through
/api/platform/webhooks. The TypeScript SDK defines the bearer management
contract under /v1/webhooks and /v1/webhook-deliveries. Those bearer routes
are active for the admitted production-preview cohort.
Create an endpoint
run.acceptedrun.startedrun.progressapproval.requiredapproval.resolvedrun.completedrun.failedrun.cancelled
signing_secret with the praxa_whsec_ prefix. The secret is shown once.
Praxa stores only its SHA-256 hash and an AES-GCM-encrypted copy for delivery.
Endpoint URLs must use HTTPS on the default port with a public multi-label
hostname. Credentials, fragments, IP literals, private or reserved IP ranges,
single-label hosts, and local, internal, or test suffixes are rejected. The
delivery Worker also validates DNS answers and revalidates the destination at
every redirect hop.
Bearer management contract
These six operations are a production partner preview, not general
availability. Use only keys issued to an admitted personal tenant.
execute:write, runs:read,
runs:write, and usage:read. There is no webhook-specific scope.
The gateway performs only the first scope check. The backend is the point-of-
effect authority and rechecks every required scope. A mutation key therefore
must carry both
runs:read and runs:write, even though gateway admission is
keyed to runs:write.
Create input is strict. url must be HTTPS, event_types must contain one to
eight unique values, and optional description must contain 1–256 characters.
Update input is also strict and must contain at least one of url,
event_types, description, or status. Set description to null to clear
it. Set status to active or disabled.
An endpoint has apiVersion, id, url, event_types, optional
description, status, createdAt, and updatedAt. Create returns that
endpoint plus the one-time signing_secret. List returns apiVersion and
data and never returns a secret.
Filter delivery records with endpoint_id, run_id, status, limit, and
cursor. limit defaults to 50 and must be between 1 and 100. Delivery status
is one of pending, delivering, succeeded, retrying, or dead_letter.
Each delivery has apiVersion, id, endpoint_id, event_type, optional
run_id, status, attempt_count, optional response_status, optional
next_attempt_at, optional delivered_at, createdAt, and updatedAt. A list
returns apiVersion, data, and optional nextCursor. Replay requires the
exact body {}.
Signed-in portal routes
The signed-in management routes are:
The queue records
pending, delivering, succeeded, retrying, and
dead_letter states. It makes at most five attempts, with delays of 30 seconds,
5 minutes, 30 minutes, and 2 hours between retries. A replay creates another
delivery attempt for the same immutable event; it does not create a new event.
Strict v1 payload
Every body has exactly these seven top-level fields:event_idis the immutable task-ledger event id. Retries and manual replay preserve it.sequenceincreases within one run. It is not a global delivery sequence.datais strict and type-specific. A v1 consumer must not assume that an additive field can appear without explicit contract-version negotiation.
Here,
<run status> is one of queued, running, waiting_approval,
completed, failed, or cancelled. The webhook contract uses
waiting_approval; the public run resource uses awaiting_approval.
<decision> is approved or denied.
Reconciliation emits strict run.progress with only
data.status: "running". The explanatory messages available on the SSE stream
are not added to the webhook body. A permanently unknown committing outcome
emits run.failed with:
error_code: "run_failed".
Approval notifications are digest-only
Anapproval.required webhook deliberately contains only the run id, event
identity, ordering fields, status, and action_digest. It does not contain an
approval id or a human-readable summary.
Before presenting a choice to a person:
- Fetch
GET /v1/runs/:idusing the webhook’srun_id. - Require an exact
pendingApprovalobject. - Verify that
pendingApproval.action_digestequals the webhook digest. - Render
pendingApproval.summaryexactly, without truncating or rewriting it. - Send the unchanged digest and the public request decision
approveordeny.
approve or deny. The emitted
approval.resolved.data.decision webhook preserves the internal event values
approved or denied.
If the pending approval is missing, stale, or mismatched, do not show an active
approval control and do not submit a decision. See
Approvals for the fail-closed contract.
Signing contract
The delivery Worker signs this canonical string with HMAC-SHA-256:event_id.
Ordering and idempotency
Webhook delivery is at least once. A failed request, timeout, retry, or manual replay can deliver the same event again, and a delayed retry can arrive after a newer event.- Deduplicate by immutable
event_id. - Process or order state changes per run by
(run_id, sequence). - Return a 2xx response only after you have durably stored the event or queued the work it triggers.
Production boundary
Automatic task-event projection is forward-only. The production migration installs anAFTER INSERT trigger, so it projects task-ledger events inserted
after the migration is applied. It does not backfill historical events.
A production canary created a tenant endpoint, received a delivery, and
verified the HMAC against the exact raw body. Retry, dead-letter, eligible
replay, digest-only approval fetch, and run_outcome_unknown remain separate
operational canaries; do not infer those results from the successful signed
delivery.