Actions, queries and webhooks
Call a server action (server/*.ts), run a named query (db/queries/*.sql), deliver an inbound webhook to a workflow, and read workflow runs and notifications.
On this page
Base URL: your app's origin ($APP_ORIGIN): the dev site (https://dev-<slug>.apps.appbricx.com), the published host or a custom domain. Errors answer { ok: false, error: { code, message } } unless noted. The host decides the environment: the dev site reads and writes the dev database, the published host and custom domains production's.
Authentication
- App key + data marker
- A project key (dpk_c_… in browsers, bound to the app's origins and sent with Origin; dpk_s_… on servers) 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 signed-out visitor. A server key may assert x-appbricx-app-user and x-appbricx-app-role.
Authorization: Bearer $APP_KEYx-appbricx-data-api: 1x-appbricx-app-session: $APP_SESSION (optional: acts as that user) - Server key
- A server-tier project key (dpk_s_…). Browser keys and end-user sessions are refused. Treat it like a database password.
Authorization: Bearer $SERVER_KEYx-appbricx-data-api: 1 - Webhook signature
- No bearer token. Sign the raw body with the webhook's secret (HMAC-SHA256, hex), or, for webhook(name, { verify: "stripe" | "github" | "razorpay" }), send the provider's own signature header.
x-appbricx-signature: <hex HMAC-SHA256 of the raw body>
Endpoints
List named queries
GET/__appbricx/queries
The app's named queries (db/queries/*.sql) with their params.
- Auth
- App key + data marker:
Authorization: Bearer $APP_KEYx-appbricx-data-api: 1x-appbricx-app-session: $APP_SESSION (optional: acts as that user)
Response
{
"ok": true,
"queries": [
{
"name": "experience-bookings",
"params": {
"experience": {
"type": "uuid",
"required": true
}
}
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer. |
| 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 a minute, 1,200 with a server key; a quarter of that per IP and per user). |
curl "$APP_ORIGIN/__appbricx/queries" \
-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/queries`, {
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: internal/svc/runtime/data/http.go:185
Run a named query
POST/__appbricx/queries/*
Run db/queries/<name>.sql with typed params, as the caller (row-level security applies). The body is the params. -- @roles limits who may call it.
- 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 |
|---|---|---|
*required | string | The query's file stem. |
Body
| Name | Type | Description |
|---|---|---|
(params)optional | object | Values for the query's -- @param lines (checked against their Postgres types). |
Response
{
"ok": true,
"rows": [
{
"bookings": 2,
"people": 20
}
],
"rowCount": 1,
"fields": [
{
"name": "bookings",
"type": "integer"
},
{
"name": "people",
"type": "integer"
}
],
"truncated": false
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer. |
| 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 a minute, 1,200 with a server key; a quarter of that per IP and per user). |
| 400 | PARAMS_INVALID | A missing or mistyped param ("param experience must be a uuid"). |
| 403 | none | The caller's role isn't in the query's @roles. |
| 404 | none | No such query. |
curl -X POST "$APP_ORIGIN/__appbricx/queries/experience-bookings" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-data-api: 1" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{"experience":"e1a2b3c4-…"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/queries/experience-bookings`, {
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({
"experience": "e1a2b3c4-…"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: internal/svc/runtime/data/http.go:186
Call a server action
POST/__appbricx/actions/:name
Run an exported defineAction of server/*.ts with the body as its input, as the caller, after the input is checked against its schema. This is what a page's await confirmBooking({ … }) sends. An Idempotency-Key header (up to 128 characters) makes a retry answer the first call's result instead of running again.
- 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 action's export name. |
Body
| Name | Type | Description |
|---|---|---|
(input)required | object | The action's input. |
Response
{
"ok": true,
"result": {
"ref": "BK-ACME1",
"status": "confirmed"
},
"runId": "4f0a…"
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer. |
| 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 a minute, 1,200 with a server key; a quarter of that per IP and per user). |
| 404 | ACTION_NOT_FOUND | No such action. |
| 401 | SIGN_IN_REQUIRED | Signed out and the action isn't open to "guest". |
| 403 | ROLE_FORBIDDEN | The caller's role isn't in the action's roles. |
| 422 | FUNCTION_FAILED | Bad input ("booking: must be an id") or the handler threw: its message. |
curl -X POST "$APP_ORIGIN/__appbricx/actions/confirmBooking" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-data-api: 1" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{"booking":"5d0c2f9e-0d4b-4c39-9f63-2a1d5b7e8f10"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/actions/confirmBooking`, {
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({
"booking": "5d0c2f9e-0d4b-4c39-9f63-2a1d5b7e8f10"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: internal/svc/runtime/data/http.go:187
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. |
| 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 a minute, 1,200 with a server key; a quarter of that per IP and per user). |
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: internal/svc/runtime/data/http.go:202
List runs
GET/__appbricx/runtime/runs
Recent workflow and action 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. |
| 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 a minute, 1,200 with a server key; a quarter of that per IP and per user). |
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: internal/svc/runtime/data/http.go:207
Get a run
GET/__appbricx/runtime/runs/:id
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 |
|---|---|---|
:idrequired | string | The run id (from an action call's runId, or the runs list). |
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. |
| 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 a minute, 1,200 with a server key; a quarter of that per IP and per user). |
| 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: internal/svc/runtime/data/http.go:208
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. |
| 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 a minute, 1,200 with a server key; a quarter of that per IP and per user). |
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: internal/svc/runtime/data/http.go:209
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. |
| 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 a minute, 1,200 with a server key; a quarter of that per IP and per user). |
| 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: internal/svc/runtime/data/http.go:210
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. |
| 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 a minute, 1,200 with a server key; a quarter of that per IP and per user). |
| 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: internal/svc/runtime/data/http.go:214
Inbound webhook (platform host)
POST/hooks/:project/:name
The same delivery at a fixed address on the platform: the URL get_contract lists for each webhook, to paste into Stripe, Razorpay or GitHub.
- Auth
- Webhook signature:
x-appbricx-signature: <hex HMAC-SHA256 of the raw body>
Path parameters
| Name | Type | Description |
|---|---|---|
:projectrequired | string | The project id. |
:namerequired | string | The webhook's name. |
Body
| Name | Type | Description |
|---|---|---|
(any JSON)optional | object | The payload. |
Response
{
"ok": true,
"runId": "4f0a…"
}Errors
| Status | Code | When |
|---|---|---|
| 404 | WEBHOOK_NOT_FOUND | Unknown or paused. |
| 401 | UNAUTHORIZED | Signature missing or wrong. |
| 503 | WEBHOOK_SECRET_NOT_SET | A provider-verified hook whose secret isn't set yet. |
curl -X POST "$APP_ORIGIN/hooks/3b7e9a52-6c1d-4f0e-9a8b-2c4d6e8f0a1b/stripe" \
-H "x-appbricx-signature: $SIGNATURE" \
-H "content-type: application/json" \
-d '{"id":"evt_1","type":"checkout.session.completed"}'const res = await fetch(`${APP_ORIGIN}/hooks/3b7e9a52-6c1d-4f0e-9a8b-2c4d6e8f0a1b/stripe`, {
method: "POST",
headers: {
"x-appbricx-signature": signature,
"content-type": "application/json",
},
body: JSON.stringify({
"id": "evt_1",
"type": "checkout.session.completed"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: internal/svc/workflows/transport/http.go:26
Inbound webhook (app host)
POST/__appbricx/hooks/:name
Deliver an event to the workflow declared with webhook(name, { verify }) in workflows/*.ts, on the app's own host. Without verify, sign the raw body with the webhook's secret; with verify: "stripe", "github" or "razorpay", send the provider's own signature. Each event id runs once.
- Auth
- Webhook signature:
x-appbricx-signature: <hex HMAC-SHA256 of the raw body>
Path parameters
| Name | Type | Description |
|---|---|---|
:namerequired | string | The webhook's name. |
Body
| Name | Type | Description |
|---|---|---|
(any JSON)optional | object | The payload (1 MB max). |
Response
{
"ok": true,
"runId": "4f0a…"
}Errors
| Status | Code | When |
|---|---|---|
| 404 | WEBHOOK_NOT_FOUND | Unknown, or the workflow is paused. |
| 401 | UNAUTHORIZED | Signature missing or wrong. |
| 413 | BODY_TOO_LARGE | Over 1 MB. |
| 400 | INVALID_JSON | Body isn't JSON. |
| 429 | RATE_LIMITED | Too many deliveries. |
curl -X POST "$APP_ORIGIN/__appbricx/hooks/new-lead" \
-H "x-appbricx-signature: $SIGNATURE" \
-H "content-type: application/json" \
-d '{"name":"Asha","email":"asha@example.com","message":"Offsite for 40?"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/hooks/new-lead`, {
method: "POST",
headers: {
"x-appbricx-signature": signature,
"content-type": "application/json",
},
body: JSON.stringify({
"name": "Asha",
"email": "asha@example.com",
"message": "Offsite for 40?"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: internal/svc/workflows/transport/http.go:52