Webhooks
Webhooks let a Fictix runtime notify your application when simulation, data, or test events occur. Use webhooks when your product needs to react to a generated company over time instead of polling event endpoints.
Webhook management currently lives on the runtime org surface:
/api/v1/orgs/{slug}/webhooks
Start in the Fictix Console: the company owner can confirm the project, plan entitlement, runtime key, and webhook configuration there.
Keep subscription creation and test delivery server-side; never expose the project key or any other credential in browser code.
The slug is returned from the demo job resource at:
{
"runtime": {
"slug": "acme-logistics"
}
}
Entitlement
Webhook subscriptions require the webhooks entitlement and currently require a
plan with the webhooks feature enabled (Developer, Pro, or Enterprise in the
default catalog).
If the project is not entitled, the API returns 403.
Authentication
Create, delete, and test are Console/org-admin actions. The signed-in Fictix Console manages a WorkOS-backed bearer session, and a direct authenticated WorkOS org administrator may use that session type for these operations:
Authorization: Bearer <workos_access_token>
The Console owns its session; it is not an external integration credential to
source, copy, or forward. External server-to-server integrations use the
Fictix-supplied X-Admin-Key on the demo-job control plane, not a copied or
invented management bearer.
Listing accepts either an authenticated WorkOS org-user bearer session or the project runtime key issued by the Console:
X-API-Key: <project_api_key>
Keep webhook management server-side. Do not expose the project runtime API key or any other credential in browser code.
Create a webhook
/api/v1/orgs/{slug}/webhooksPOST /api/v1/orgs/{slug}/webhooks
Content-Type: application/json
Authorization: Bearer <workos_access_token>
Request:
{
"url": "https://example.test/fictix/webhooks",
"events": ["fictix.test", "invoice.issued"],
"secret": "whsec_32_bytes_or_more"
}
Fields:
| Field | Type | Required | Notes |
|---|---|---|---|
url |
URL | Yes | HTTPS callback URL. The server validates callback safety before storing it. |
events |
string array or null | No | null or omitted subscribes to all events. A provided list is capped to the first 50 event names. |
secret |
string or null | No | Shared signing secret. Store a high-entropy value. Fictix does not return it later. |
Response:
{
"id": "0b96f8220f0c46c9a6c33c77",
"company_id": "proj_123",
"url": "https://example.test/fictix/webhooks",
"events": ["fictix.test", "invoice.issued"],
"has_secret": true,
"active": true,
"created_at": "2026-07-01T10:15:30.000000+00:00",
"updated_at": "2026-07-01T10:15:30.000000+00:00"
}
Secrets are never returned. The response uses has_secret so your UI can show
whether signing is configured without leaking the secret.
List webhooks
/api/v1/orgs/{slug}/webhooksGET /api/v1/orgs/{slug}/webhooks
X-API-Key: <project_api_key>
Response:
{
"webhooks": [
{
"id": "0b96f8220f0c46c9a6c33c77",
"company_id": "proj_123",
"url": "https://example.test/fictix/webhooks",
"events": null,
"has_secret": true,
"active": true,
"created_at": "2026-07-01T10:15:30.000000+00:00",
"updated_at": "2026-07-01T10:15:30.000000+00:00"
}
],
"count": 1
}
events: null means the subscription receives every event type.
Delete a webhook
/api/v1/orgs/{slug}/webhooks/{webhook_id}DELETE /api/v1/orgs/{slug}/webhooks/{webhook_id}
Authorization: Bearer <workos_access_token>
Response:
{
"deleted": true,
"webhook_id": "0b96f8220f0c46c9a6c33c77"
}
Deleting a webhook removes the subscription. It does not remove historical events from the Fictix event log.
Send a test event
/api/v1/orgs/{slug}/webhooks/{webhook_id}/testPOST /api/v1/orgs/{slug}/webhooks/{webhook_id}/test
Content-Type: application/json
Authorization: Bearer <workos_access_token>
Request:
{
"event_type": "fictix.test"
}
Response:
{
"queued": true,
"webhook_id": "0b96f8220f0c46c9a6c33c77",
"event_type": "fictix.test",
"url": "https://example.test/fictix/webhooks"
}
The response means delivery was queued or started. Your receiver should still validate that the callback arrived.
Delivery headers
Webhook POST requests include:
Content-Type: application/json
X-Fictix-Event: <event_type>
X-Fictix-Timestamp: <iso_timestamp>
X-Fictix-Signature: sha256=<hex_hmac>
X-Fictix-Signature is present only when the subscription has a secret.
The signature is HMAC-SHA256 over the raw request body bytes using the secret stored on the subscription.
Verify a signature
Node example:
import crypto from "node:crypto";
export function verifyFictixWebhook({ rawBody, signature, secret }) {
if (!signature || !signature.startsWith("sha256=")) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = signature.slice("sha256=".length);
return crypto.timingSafeEqual(
Buffer.from(received, "hex"),
Buffer.from(expected, "hex")
);
}
Important implementation detail: verify against the raw body bytes, not a JSON-reencoded object. Reencoding can change whitespace or key order and break a valid signature.
Payload shape
Test payloads use this envelope:
{
"id": "test_1720746930",
"event_type": "fictix.test",
"company_id": "proj_123",
"data": {
"message": "This is a test event from Fictix webhooks"
},
"timestamp": "2026-07-11T10:15:30.000000+00:00"
}
Persisted simulation and runtime events use the canonical envelope:
{
"id": "evt_abc123",
"company_id": "proj_123",
"environment_id": null,
"schema_version": "1.0",
"event_type": "invoice.issued",
"occurred_at": "2026-07-11T10:15:30.000000+00:00",
"recorded_at": "2026-07-11T10:15:31.000000+00:00",
"actor_kind": null,
"actor_id": null,
"entity_type": "invoice",
"entity_id": "inv_123",
"correlation_id": "corr_abc123",
"parent_id": null,
"data": {},
"mutation": null,
"projection_refs": [],
"audit_meta": null,
"layer": 1
}
The stable fields are id, company_id, schema_version, event_type,
occurred_at, and data. Treat all other fields as nullable or additive unless
your integration needs them. Query the event stream with your project key at:
/api/v1/orgs/{slug}/eventsGET /api/v1/orgs/{slug}/events
Treat unknown event types as forward-compatible. Store the event, log it, and avoid failing the whole receiver.
Delivery semantics
Fictix delivers webhooks asynchronously.
Current delivery behavior:
- callback URLs are SSRF-guarded before storage and before delivery
- event fanout is tenant scoped
- active subscriptions are filtered by event type
- deliveries are queued in a durable outbox
- the worker claims due deliveries in batches
- successful delivery is any
2xxresponse - failed delivery is retried with backoff
- stuck
sendingrows can be recovered by the worker
Each worker delivery uses up to 3 immediate POST attempts. If that worker delivery fails, the durable outbox schedules another worker attempt with a 1, 2, 4, then 8 second backoff. The delivery becomes terminally failed after 5 worker attempts. A receiver must still be idempotent: network retries, the three immediate attempts, and worker recovery can produce duplicate callbacks.
Receivers must be idempotent. Network retries and worker recovery can produce duplicate deliveries. Deduplicate by event id.
Subscription limits
Each org can have up to 20 active webhook subscriptions.
If the limit is exceeded:
422 Unprocessable Entity
{
"detail": "Maximum 20 webhook subscriptions per org"
}
The events filter stores at most 50 event names per subscription.
Receiver requirements
Your receiver should:
- respond within 10 seconds
- return a
2xxstatus only after accepting the event - verify
X-Fictix-Signaturewhen a secret is configured - deduplicate by event id
- reject stale timestamps if your threat model requires replay protection
- avoid fetching user-controlled URLs from the payload
- log event id, type, company id, and delivery timestamp
Do not perform long-running ingestion synchronously inside the webhook handler. Ack the event, enqueue work in your own system, and process it out of band.
Current event types
fictix.test is the manual receiver test event. The current canonical financial
event types include:
invoice.issued
payment.received
credit_note.issued
refund.issued
bill.received
bill_payment.sent
card_transaction.posted
reimbursement.submitted
bank_deposit.settled
bank_withdrawal.settled
bank_transfer.completed
Use exact values returned by your project's event stream when subscribing. Unknown event types may be added without a breaking API version, so retain and log them rather than rejecting an otherwise valid delivery.
Troubleshooting
No callbacks arrive:
- confirm the project has the
webhooksentitlement - confirm the subscription is active
- confirm the event filter includes the emitted event type
- send
fictix.test - check that your callback URL is publicly reachable over HTTPS
- check that your receiver returns a
2xxresponse
Signatures fail:
- verify against raw request bytes
- ensure the secret is the exact value used when the subscription was created
- do not trim, parse, or reserialize the body before verification
Duplicate callbacks:
- expected under retry and recovery semantics
- deduplicate by event id
- make downstream processing idempotent
