Skip to main content
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

Allowed event types are:
  • run.accepted
  • run.started
  • run.progress
  • approval.required
  • approval.resolved
  • run.completed
  • run.failed
  • run.cancelled
The list must contain one to eight unique values. A tenant can have at most 20 active endpoints. The create response contains the endpoint plus a 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.
The key scope vocabulary remains exactly 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_id is the immutable task-ledger event id. Retries and manual replay preserve it.
  • sequence increases within one run. It is not a global delivery sequence.
  • data is 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:
Do not retry or resubmit a run automatically after that outcome. Ordinary failed, partial, and expired outcomes use error_code: "run_failed".

Approval notifications are digest-only

An approval.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:
  1. Fetch GET /v1/runs/:id using the webhook’s run_id.
  2. Require an exact pendingApproval object.
  3. Verify that pendingApproval.action_digest equals the webhook digest.
  4. Render pendingApproval.summary exactly, without truncating or rewriting it.
  5. Send the unchanged digest and the public request decision approve or deny.
The decision vocabularies intentionally differ across the boundary. The public approval request accepts 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:
It sends:
Verify the signature against the exact raw body before parsing JSON, use a constant-time comparison, enforce a short timestamp tolerance, and then verify that the header id equals the body’s 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.
  1. Deduplicate by immutable event_id.
  2. Process or order state changes per run by (run_id, sequence).
  3. Return a 2xx response only after you have durably stored the event or queued the work it triggers.
Do not use arrival order, attempt count, or HTTP request identity as run order.

Production boundary

Automatic task-event projection is forward-only. The production migration installs an AFTER 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.
Last modified on August 14, 2026