Functions, workflows and webhooks
Call server functions, queue workflow runs, read runs and dead letters, read notifications, and receive inbound webhooks.
On this page
Base URL: your app's origin ($APP_ORIGIN): the published host, a custom domain, or the platform origin for the builder preview. Errors answer { ok: false, error: { code, message } } unless noted. Add x-appbricx-env: dev or prod to pick the database explicitly; otherwise the host decides (preview means dev, a published host means prod).
Authentication
- App key + data marker
- A project API key (dpk_c_… in browsers, bound to the app's origins; dpk_s_… on servers) or the builder preview token, plus the data-API marker header. With an app session (the header, or the appbricx_app_session cookie) the call acts as that user under row-level security; without one it acts as a guest. A server key may assert x-appbricx-app-user, x-appbricx-app-role and x-appbricx-app-org.
Authorization: Bearer $APP_KEYx-appbricx-data-api: 1x-appbricx-app-session: $APP_SESSION (optional: acts as that user) - Server key
- A server-tier project API key (dpk_s_…, created in the editor under Backend, API). Browser keys and end-user sessions are refused.
Authorization: Bearer $SERVER_KEYx-appbricx-data-api: 1 - Webhook signature
- No bearer token. When the webhook has a secret, sign the raw body with HMAC-SHA256 and send the hex digest. Hooks with a caller-provisioned secret also accept the secret itself in x-appbricx-webhook-secret.
x-appbricx-signature: <hex HMAC-SHA256 of the raw body>
Endpoints
Call a server function
POST/__appbricx/functions/:name
Run the workflow name (declared with on: { call: true }) now, for the signed-in caller, and wait for its return value (at most about 35 seconds).
- Auth
- App key + data marker:
Authorization: Bearer $APP_KEYx-appbricx-data-api: 1x-appbricx-app-session: $APP_SESSION (optional: acts as that user)
Path parameters
| Name | Type | Description |
|---|---|---|
:namerequired | string | The workflow id. |
Body
| Name | Type | Description |
|---|---|---|
inputoptional | object | The function's input (64 KB max). |
Response
{
"ok": true,
"result": {
"total": 7.9,
"currency": "EUR"
},
"runId": "4f0a…"
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | PARAMS_INVALID | The x-appbricx-data-api: 1 header is missing. |
| 403 | ORIGIN_NOT_ALLOWED | A browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all). |
| 429 | RATE_LIMITED | The project's per-minute request budget is spent (600 for preview tokens and browser keys, 1200 for server keys by default). |
| 400 | BAD_FUNCTION_NAME | Invalid name. |
| 400 | BAD_JSON | Body isn't JSON. |
| 400 | BAD_INPUT | input isn't an object. |
| 413 | INPUT_TOO_LARGE | Over 64 KB. |
| 404 | FUNCTION_NOT_FOUND | No such workflow. |
| 403 | NOT_A_FUNCTION | Not a ui-triggered workflow. |
| 401 | SIGN_IN_REQUIRED | Signed out and the function isn't public. |
| 403 | ROLE_FORBIDDEN | The caller's role isn't in the function's roles. |
| 429 | FUNCTION_BUSY | 4 calls already in flight for this app. |
| 429 | QUOTA_EXCEEDED | Daily workflow runs used up. |
| 422 | FUNCTION_FAILED | The function threw; message is its error (credentials masked). |
| 504 | FUNCTION_TIMEOUT | Didn't finish in time. |
curl -X POST "$APP_ORIGIN/__appbricx/functions/shipping-quote" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-data-api: 1" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{"input":{"country":"DE","weightKg":2.5}}'const res = await fetch(`${APP_ORIGIN}/__appbricx/functions/shipping-quote`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-data-api": "1",
"x-appbricx-app-session": APP_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"input": {
"country": "DE",
"weightKg": 2.5
}
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/app-runtime/http/workflow-routes.ts:121
Queue a workflow run
POST/__appbricx/runtime/workflows/:id/run
Queue a ui-triggered workflow (other triggers need a server key). The run's identity comes from the session, never from the payload.
- Auth
- App key + data marker:
Authorization: Bearer $APP_KEYx-appbricx-data-api: 1x-appbricx-app-session: $APP_SESSION (optional: acts as that user)
Path parameters
| Name | Type | Description |
|---|---|---|
:idrequired | string | The workflow id. |
Body
| Name | Type | Description |
|---|---|---|
payloadoptional | object | The event payload. |
dryRunoptional | boolean | Trace without side effects. |
Response
{
"ok": true,
"runId": "4f0a…"
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | PARAMS_INVALID | The x-appbricx-data-api: 1 header is missing. |
| 403 | ORIGIN_NOT_ALLOWED | A browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all). |
| 429 | RATE_LIMITED | The project's per-minute request budget is spent (600 for preview tokens and browser keys, 1200 for server keys by default). |
| 403 | WORKFLOW_NOT_UI_TRIGGERED | Not ui-triggered and not a server key. |
| 401 | SIGN_IN_REQUIRED | A callable function needs a signed-in caller here. |
| 403 | ROLE_FORBIDDEN | Role not allowed. |
| 404 | PROJECT_NOT_FOUND | Project gone. |
curl -X POST "$APP_ORIGIN/__appbricx/runtime/workflows/send-invoice/run" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-data-api: 1" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{"payload":{"order":"8c1d…"}}'const res = await fetch(`${APP_ORIGIN}/__appbricx/runtime/workflows/send-invoice/run`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-data-api": "1",
"x-appbricx-app-session": APP_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"payload": {
"order": "8c1d…"
}
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/app-runtime/http/workflow-routes.ts:200
Get a run
GET/__appbricx/runtime/runs/:runId
One workflow run with its status, error and log lines.
- Auth
- App key + data marker:
Authorization: Bearer $APP_KEYx-appbricx-data-api: 1x-appbricx-app-session: $APP_SESSION (optional: acts as that user)
Path parameters
| Name | Type | Description |
|---|---|---|
:runIdrequired | string | From a queue or function call. |
Response
{
"ok": true,
"run": {
"id": "4f0a…",
"workflow_id": "send-invoice",
"status": "succeeded",
"error": null,
"started_at": "2026-10-12T09:14:03Z",
"finished_at": "2026-10-12T09:14:04Z",
"logs": [
{
"ts": "…",
"level": "info",
"message": "invoice sent"
}
]
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | PARAMS_INVALID | The x-appbricx-data-api: 1 header is missing. |
| 403 | ORIGIN_NOT_ALLOWED | A browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all). |
| 429 | RATE_LIMITED | The project's per-minute request budget is spent (600 for preview tokens and browser keys, 1200 for server keys by default). |
| 404 | RUN_NOT_FOUND | Unknown run. |
curl "$APP_ORIGIN/__appbricx/runtime/runs/4f0a1c2d-…" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-data-api: 1" \
-H "x-appbricx-app-session: $APP_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/runtime/runs/4f0a1c2d-…`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-data-api": "1",
"x-appbricx-app-session": APP_SESSION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/app-runtime/http/workflow-routes.ts:264
List runs
GET/__appbricx/runtime/runs
Recent workflow runs of the project.
- Auth
- App key + data marker:
Authorization: Bearer $APP_KEYx-appbricx-data-api: 1x-appbricx-app-session: $APP_SESSION (optional: acts as that user)
Query parameters
| Name | Type | Description |
|---|---|---|
limitoptional | number | 1 to 200 (default 50). |
statusoptional | string | queued, running, succeeded, failed… |
Response
{
"ok": true,
"runs": [
{
"id": "4f0a…",
"workflow_id": "send-invoice",
"trigger_type": "manual",
"status": "failed",
"error": "SMTP timeout",
"attempt": 3,
"call_depth": 0,
"created_at": "…"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | PARAMS_INVALID | The x-appbricx-data-api: 1 header is missing. |
| 403 | ORIGIN_NOT_ALLOWED | A browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all). |
| 429 | RATE_LIMITED | The project's per-minute request budget is spent (600 for preview tokens and browser keys, 1200 for server keys by default). |
curl "$APP_ORIGIN/__appbricx/runtime/runs?limit=20&status=failed" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-data-api: 1" \
-H "x-appbricx-app-session: $APP_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/runtime/runs?limit=20&status=failed`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-data-api": "1",
"x-appbricx-app-session": APP_SESSION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/app-runtime/http/workflow-routes.ts:278
Notifications
GET/__appbricx/notifications
The last notifications for the caller: person-targeted ones for them, role-targeted ones for their role, and untargeted ones.
- Auth
- App key + data marker:
Authorization: Bearer $APP_KEYx-appbricx-data-api: 1x-appbricx-app-session: $APP_SESSION (optional: acts as that user)
Query parameters
| Name | Type | Description |
|---|---|---|
limitoptional | number | 1 to 200 (default 50). |
Response
{
"ok": true,
"notifications": [
{
"id": "n_1",
"kind": null,
"title": "New order",
"body": "Order 1042 needs packing",
"meta": {
"role": "staff",
"user": null
},
"created_at": "…"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | PARAMS_INVALID | The x-appbricx-data-api: 1 header is missing. |
| 403 | ORIGIN_NOT_ALLOWED | A browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all). |
| 429 | RATE_LIMITED | The project's per-minute request budget is spent (600 for preview tokens and browser keys, 1200 for server keys by default). |
curl "$APP_ORIGIN/__appbricx/notifications?limit=20" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-data-api: 1" \
-H "x-appbricx-app-session: $APP_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/notifications?limit=20`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-data-api": "1",
"x-appbricx-app-session": APP_SESSION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/app-runtime/http/workflow-routes.ts:315
Dead letters
GET/__appbricx/runtime/dlq
Jobs that used up their retries.
- Auth
- App key + data marker:
Authorization: Bearer $APP_KEYx-appbricx-data-api: 1x-appbricx-app-session: $APP_SESSION (optional: acts as that user)
Query parameters
| Name | Type | Description |
|---|---|---|
limitoptional | number | 1 to 1000 (default 100). |
Response
{
"ok": true,
"jobs": [
{
"id": "j_9",
"run_id": "4f0a…",
"attempt": 3,
"max_attempts": 3,
"last_error": "SMTP timeout",
"workflow_id": "send-invoice",
"trigger_type": "cdc"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | PARAMS_INVALID | The x-appbricx-data-api: 1 header is missing. |
| 403 | ORIGIN_NOT_ALLOWED | A browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all). |
| 429 | RATE_LIMITED | The project's per-minute request budget is spent (600 for preview tokens and browser keys, 1200 for server keys by default). |
curl "$APP_ORIGIN/__appbricx/runtime/dlq?limit=50" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-data-api: 1" \
-H "x-appbricx-app-session: $APP_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/runtime/dlq?limit=50`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-data-api": "1",
"x-appbricx-app-session": APP_SESSION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/app-runtime/http/workflow-routes.ts:353
Replay a dead letter
POST/__appbricx/runtime/dlq/replay
Queue a dead job again.
- Auth
- App key + data marker:
Authorization: Bearer $APP_KEYx-appbricx-data-api: 1x-appbricx-app-session: $APP_SESSION (optional: acts as that user)
Body
| Name | Type | Description |
|---|---|---|
jobIdrequired | string | From the dead-letter list. |
Response
{
"ok": true,
"replayed": true
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | PARAMS_INVALID | The x-appbricx-data-api: 1 header is missing. |
| 403 | ORIGIN_NOT_ALLOWED | A browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all). |
| 429 | RATE_LIMITED | The project's per-minute request budget is spent (600 for preview tokens and browser keys, 1200 for server keys by default). |
| 400 | MISSING_JOB_ID | No jobId. |
| 404 | JOB_NOT_FOUND | Unknown job. |
| 403 | CROSS_PROJECT | Another project's job. |
| 400 | JOB_NOT_DEAD | Not in the dead state. |
curl -X POST "$APP_ORIGIN/__appbricx/runtime/dlq/replay" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-data-api: 1" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{"jobId":"j_9"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/runtime/dlq/replay`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-data-api": "1",
"x-appbricx-app-session": APP_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"jobId": "j_9"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/app-runtime/http/workflow-routes.ts:377
Webhook secret
GET/__appbricx/runtime/webhooks/:name/secret
The derived signing secret of a webhook, to paste into the sending provider's HMAC settings.
- Auth
- Server key:
Authorization: Bearer $SERVER_KEYx-appbricx-data-api: 1
Path parameters
| Name | Type | Description |
|---|---|---|
:namerequired | string | The webhook name. |
Response
{
"ok": true,
"secret": "5f2c…e91a"
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | PARAMS_INVALID | The x-appbricx-data-api: 1 header is missing. |
| 403 | ORIGIN_NOT_ALLOWED | A browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all). |
| 429 | RATE_LIMITED | The project's per-minute request budget is spent (600 for preview tokens and browser keys, 1200 for server keys by default). |
| 403 | TIER_FORBIDDEN | Not a server key. |
| 400 | INVALID_NAME | Bad name. |
| 404 | WEBHOOK_NOT_FOUND | No such webhook. |
| 400 | NOT_DERIVED | The webhook uses your own env-var secret. |
curl "$APP_ORIGIN/__appbricx/runtime/webhooks/stripe-events/secret" \
-H "Authorization: Bearer $SERVER_KEY" \
-H "x-appbricx-data-api: 1"const res = await fetch(`${APP_ORIGIN}/__appbricx/runtime/webhooks/stripe-events/secret`, {
headers: {
Authorization: `Bearer ${SERVER_KEY}`,
"x-appbricx-data-api": "1",
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/app-runtime/http/workflow-routes.ts:415
Inbound webhook
POST/hooks/:projectId/:name
Receive an event from another service and queue the bound workflow with { webhook, body, headers }. The environment (dev or prod) is the one bound when the webhook was registered.
- Auth
- Webhook signature:
x-appbricx-signature: <hex HMAC-SHA256 of the raw body>
Path parameters
| Name | Type | Description |
|---|---|---|
:projectIdrequired | string | The project id. |
:namerequired | string | The webhook name. |
Body
| Name | Type | Description |
|---|---|---|
(any JSON)optional | object | The provider's payload (1 MB max). |
Response
A repeated x-appbricx-idempotency-key within 24 hours answers { ok: true, idempotent: true } without a second run.
{
"ok": true,
"runId": "4f0a…"
}Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_WEBHOOK_NAME | Bad name. |
| 429 | RATE_LIMITED | Too many deliveries. |
| 413 | BODY_TOO_LARGE | Over 1 MB. |
| 404 | WEBHOOK_NOT_FOUND | Unknown or disabled. |
| 400 | INVALID_JSON | Body isn't JSON. |
| 401 | UNAUTHORIZED | Signature (or secret) missing or wrong. |
curl -X POST "$APP_ORIGIN/hooks/3b7e…/stripe-events" \
-H "x-appbricx-signature: $SIGNATURE" \
-H "content-type: application/json" \
-d '{"type":"checkout.session.completed","data":{"object":{"id":"cs_…"}}}'const res = await fetch(`${APP_ORIGIN}/hooks/3b7e…/stripe-events`, {
method: "POST",
headers: {
"x-appbricx-signature": signature,
"content-type": "application/json",
},
body: JSON.stringify({
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_…"
}
}
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/app-runtime/http/hooks-routes.ts:184