01 · Start
Getting started
Organization owners and admins can configure endpoints in the protected Webhook Management Portal. The create response reveals the signing secret once; store it immediately. Service integrations can use the API directly.
curl -X POST "https://psymind.app/api/v1/webhook-endpoints" \
-H "Authorization: Bearer $PSYMIND_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/psymind/webhooks","events":["practice.ready","outcome.submitted"],"active":true}'Required service scope: webhooks:write. To send a signed test through the same pipeline:
curl -X POST "https://psymind.app/api/v1/webhook-endpoints/$ENDPOINT_ID/test" \
-H "Authorization: Bearer $PSYMIND_API_TOKEN"List endpoint configuration with GET /webhook-endpoints (webhooks:read) and delivery history with GET /webhook-deliveries. See the generated create, test, and history operations.
02 · Contract
Canonical event catalog
These are the nine production event types in api/_webhook-event-registry.js. webhook.test is an internal test delivery type and is not a production domain event.
| Event | Object | Status | Fetch |
|---|---|---|---|
practice.created | practice | created | GET /practices/{id} |
practice.ready | practice | ready | GET /practices/{id} |
practice.failed | practice | failed | GET /practices/{id} |
audio.ready | audio | ready | GET /practices/{id}/audio |
audio.failed | audio | failed | GET /practices/{id}/audio |
practice.assigned | assignment | assigned | GET /assignments/{id} |
practice.started | assignment | started | GET /assignments/{id} |
practice.completed | assignment | completed | GET /assignments/{id} |
outcome.submitted | outcome | submitted | GET /assignments/{id}/outcomes |
03 · Payload
Minimal event envelope
{
"id": "evt_01J9EXAMPLE123456",
"type": "practice.ready",
"created_at": "2026-08-28T10:15:00.000Z",
"api_version": "2026-08-01",
"data": { "object": { "id": "prc_01J9EXAMPLE", "object": "practice", "status": "ready" } }
}The payload contains identifiers and lifecycle status only. Fetch authorized resource details after verifying the signature.
04 · Verify
Verify before parsing
Headers are psymind-event-id, psymind-timestamp, and psymind-signature. Compute HMAC-SHA256 over the exact bytes {timestamp}.{rawBody} using the full whsec_ secret. Accept only a v1= lowercase-hex candidate, compare in constant time, reject timestamps older/newer than 300 seconds, then require the JSON body's id to equal the event-id header.
import { verifyWebhook } from "@psymind/sdk";
const verified = verifyWebhook(rawBody, request.headers, process.env.PSYMIND_WEBHOOK_SECRET);
// Insert verified.id with a UNIQUE constraint before side effects.from psymind.webhooks import verify_webhook
verified = verify_webhook(raw_body, headers, os.environ["PSYMIND_WEBHOOK_SECRET"])
# Insert verified.id atomically; duplicates return 2xx without repeating work.Both examples use the published server-side SDK verifiers. Keep the raw request body unchanged until verification.
05 · Reliability
Delivery and retries
- Any 2xx response succeeds. Redirects, non-2xx responses, network errors, and the 10-second timeout fail.
- Seven attempts are scheduled at 0, 60, 300, 1,800, 7,200, 43,200, and 86,400 seconds after failure.
- Retries and manual replay reuse the same event id and payload. Manual retry is available only after terminal failure.
- Use
GET /webhook-deliveriesto inspect pending, processing, succeeded, or failed records;POST /webhook-deliveries/{id}/retryqueues a terminal failure. - Event and successful-delivery history is retained 30 days; failed-delivery metadata 90 days.
Do not assume ordering. Make consumers idempotent and fetch current state when event ordering matters.
06 · Security
Endpoint and secret hygiene
- Endpoints must be public HTTPS on port 443; private, loopback, link-local, reserved, mixed-DNS, and rebinding destinations are rejected.
- Secrets are encrypted server-side and returned only on creation or explicit rotation. Rotation returns plaintext once; deletion revokes the endpoint.
- Payloads are capped at 64 KiB and response previews are sanitized. Never log the signing secret or raw sensitive content.
07 · Diagnose
Common failures
| Symptom | What to check |
|---|---|
webhook_signature_mismatch | Raw bytes, exact timestamp delimiter, full secret, lowercase hex, and constant-time comparison. |
webhook_timestamp_skew | Clock synchronization and the ±300-second replay window. |
webhook_endpoint_timeout | Return a 2xx within 10 seconds; process asynchronously. |
webhook_url_private_address | Public DNS and HTTPS port 443 without redirects. |
webhook_secret_rotation_conflict | Retry the complete rotation PATCH; no plaintext is returned on the stale request. |
For operation parameters, scopes, status codes, and schemas, use the API Reference. The documentation overview links back here.