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.

11 endpointsHTTP API referenceGuide: Server actions guide

On this page
  1. Authentication
  2. Endpoints
  3. GET /queries
  4. POST /queries/*
  5. POST /actions/:name
  6. GET /notifications
  7. GET /runtime/runs
  8. GET /runtime/runs/:id
  9. GET /runtime/dlq
  10. POST /runtime/dlq/replay
  11. GET /runtime/webhooks/:name/secret
  12. POST /hooks/:project/:name
  13. POST /hooks/:name

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

Response
{
  "ok": true,
  "queries": [
    {
      "name": "experience-bookings",
      "params": {
        "experience": {
          "type": "uuid",
          "required": true
        }
      }
    }
  ]
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid Authorization bearer.
400PARAMS_INVALIDThe x-appbricx-data-api: 1 header is missing.
403ORIGIN_NOT_ALLOWEDA browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all).
429RATE_LIMITEDThe 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
curl "$APP_ORIGIN/__appbricx/queries" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "x-appbricx-data-api: 1" \
  -H "x-appbricx-app-session: $APP_SESSION"
fetch
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

NameTypeDescription
*requiredstringThe query's file stem.

Body

NameTypeDescription
(params)optionalobjectValues for the query's -- @param lines (checked against their Postgres types).

Response

Response
{
  "ok": true,
  "rows": [
    {
      "bookings": 2,
      "people": 20
    }
  ],
  "rowCount": 1,
  "fields": [
    {
      "name": "bookings",
      "type": "integer"
    },
    {
      "name": "people",
      "type": "integer"
    }
  ],
  "truncated": false
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid Authorization bearer.
400PARAMS_INVALIDThe x-appbricx-data-api: 1 header is missing.
403ORIGIN_NOT_ALLOWEDA browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all).
429RATE_LIMITEDThe 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).
400PARAMS_INVALIDA missing or mistyped param ("param experience must be a uuid").
403noneThe caller's role isn't in the query's @roles.
404noneNo such query.
curl
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-…"}'
fetch
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

NameTypeDescription
:namerequiredstringThe action's export name.

Body

NameTypeDescription
(input)requiredobjectThe action's input.

Response

Response
{
  "ok": true,
  "result": {
    "ref": "BK-ACME1",
    "status": "confirmed"
  },
  "runId": "4f0a…"
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid Authorization bearer.
400PARAMS_INVALIDThe x-appbricx-data-api: 1 header is missing.
403ORIGIN_NOT_ALLOWEDA browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all).
429RATE_LIMITEDThe 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).
404ACTION_NOT_FOUNDNo such action.
401SIGN_IN_REQUIREDSigned out and the action isn't open to "guest".
403ROLE_FORBIDDENThe caller's role isn't in the action's roles.
422FUNCTION_FAILEDBad input ("booking: must be an id") or the handler threw: its message.
curl
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"}'
fetch
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

NameTypeDescription
limitoptionalnumber1 to 200 (default 50).

Response

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

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid Authorization bearer.
400PARAMS_INVALIDThe x-appbricx-data-api: 1 header is missing.
403ORIGIN_NOT_ALLOWEDA browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all).
429RATE_LIMITEDThe 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
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"
fetch
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

NameTypeDescription
limitoptionalnumber1 to 200 (default 50).
statusoptionalstringqueued, running, succeeded, failed…

Response

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

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid Authorization bearer.
400PARAMS_INVALIDThe x-appbricx-data-api: 1 header is missing.
403ORIGIN_NOT_ALLOWEDA browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all).
429RATE_LIMITEDThe 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
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"
fetch
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

NameTypeDescription
:idrequiredstringThe run id (from an action call's runId, or the runs list).

Response

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

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid Authorization bearer.
400PARAMS_INVALIDThe x-appbricx-data-api: 1 header is missing.
403ORIGIN_NOT_ALLOWEDA browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all).
429RATE_LIMITEDThe 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).
404RUN_NOT_FOUNDUnknown run.
curl
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"
fetch
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

NameTypeDescription
limitoptionalnumber1 to 1000 (default 100).

Response

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

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid Authorization bearer.
400PARAMS_INVALIDThe x-appbricx-data-api: 1 header is missing.
403ORIGIN_NOT_ALLOWEDA browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all).
429RATE_LIMITEDThe 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
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"
fetch
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

NameTypeDescription
jobIdrequiredstringFrom the dead-letter list.

Response

Response
{
  "ok": true,
  "replayed": true
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid Authorization bearer.
400PARAMS_INVALIDThe x-appbricx-data-api: 1 header is missing.
403ORIGIN_NOT_ALLOWEDA browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all).
429RATE_LIMITEDThe 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).
400MISSING_JOB_IDNo jobId.
404JOB_NOT_FOUNDUnknown job.
403CROSS_PROJECTAnother project's job.
400JOB_NOT_DEADNot in the dead state.
curl
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"}'
fetch
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

NameTypeDescription
:namerequiredstringThe webhook name.

Response

Response
{
  "ok": true,
  "secret": "5f2c…e91a"
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid Authorization bearer.
400PARAMS_INVALIDThe x-appbricx-data-api: 1 header is missing.
403ORIGIN_NOT_ALLOWEDA browser key used from an origin it isn't bound to (ORIGIN_REQUIRED when there is no Origin at all).
429RATE_LIMITEDThe 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).
403TIER_FORBIDDENNot a server key.
400INVALID_NAMEBad name.
404WEBHOOK_NOT_FOUNDNo such webhook.
400NOT_DERIVEDThe webhook uses your own env-var secret.
curl
curl "$APP_ORIGIN/__appbricx/runtime/webhooks/stripe-events/secret" \
  -H "Authorization: Bearer $SERVER_KEY" \
  -H "x-appbricx-data-api: 1"
fetch
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

NameTypeDescription
:projectrequiredstringThe project id.
:namerequiredstringThe webhook's name.

Body

NameTypeDescription
(any JSON)optionalobjectThe payload.

Response

Response
{
  "ok": true,
  "runId": "4f0a…"
}

Errors

StatusCodeWhen
404WEBHOOK_NOT_FOUNDUnknown or paused.
401UNAUTHORIZEDSignature missing or wrong.
503WEBHOOK_SECRET_NOT_SETA provider-verified hook whose secret isn't set yet.
curl
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"}'
fetch
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

NameTypeDescription
:namerequiredstringThe webhook's name.

Body

NameTypeDescription
(any JSON)optionalobjectThe payload (1 MB max).

Response

Response
{
  "ok": true,
  "runId": "4f0a…"
}

Errors

StatusCodeWhen
404WEBHOOK_NOT_FOUNDUnknown, or the workflow is paused.
401UNAUTHORIZEDSignature missing or wrong.
413BODY_TOO_LARGEOver 1 MB.
400INVALID_JSONBody isn't JSON.
429RATE_LIMITEDToo many deliveries.
curl
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?"}'
fetch
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