Outbound Webhooks From Sapience
Send signed POST or GET calls to your own systems when users, projects, tasks or scheduled jobs change.
Overview
Webhooks let Sapience tell your own systems the moment something happens — no polling, no scheduled exports. You register a URL once, and every time a matching event occurs Sapience sends a signed HTTP request to that URL with the event's details.
You can subscribe to fifteen events, covering users, databases (rows, columns and public forms), projects, tasks and scheduled jobs (see Events You Can Subscribe To below). That covers use cases such as adding every new user to your CRM, posting in Slack when a project is completed, opening a ticket when a task is reassigned, sending the answer from a scheduled agent job straight into your own system, or telling another system the moment a row in one of your Sapience databases is added, changes, is removed or lands in a saved view, when a table's columns change, or when someone submits one of your public forms.
You manage webhooks through the Sapience API: you can register, list and delete webhooks, and ask which events are eligible. Each webhook can use its own authentication, so your endpoint only accepts calls that really come from Sapience.
Events You Can Subscribe To
Event | Fires when |
user_created | A new user account is created |
database_row_added | A row is added to a database table — created there, or linked in from another object |
database_row_updated | At least one cell value of a database row changes — carries the values before and after |
database_row_entered_view | A database row enters a saved view — it is created or linked into the view, or edited so that it now belongs there |
database_row_removed | A row is removed (unlinked) from a database table. The object the row pointed at is not deleted |
database_row_changed | Anything about a row is saved — a cell, its position (a move) or its name. Names the columns, never their values |
database_schema_changed | A table's columns change — a column is added, edited, deleted or reordered |
database_form_submitted | Someone submits one of your public database forms — carries their answers |
project_status_changed | A project is completed, archived or reactivated |
project_file_uploaded | A file is added to a project — directly, or into a folder inside it |
task_created | A task is created |
task_due_date_reached | A task's due date arrives |
task_status_changed | A task changes status — including when it is completed |
task_assignee_changed | A task is assigned, reassigned or unassigned |
scheduled_job_finished | A scheduled job finishes, either completed or failed |
A webhook listens to one event, so register one webhook per event you care about. Ask GET /api/v2/webhooks/eligible-events at any time for the live list, including the fields each event carries.
Completed tasks: there is no separate "task completed" event. Subscribe totask_status_changedand check fornew_statusequal tocompleted.
Database events: all seven database events can be narrowed to one table;database_row_addedanddatabase_row_updatedalso to rows that match conditions, anddatabase_row_entered_viewto one saved view. See Database Webhooks: Tables, Conditions And Views below.
Who Can Create Which Webhook
Every webhook has a scope, which decides whose events trigger it and who is allowed to create it.
Scope | Fires for | Who can create it |
org | Events in your organisation only | Organisation admins |
system | Events in every organisation | Sapience superusers only |
user | Events that belong to you | Any user |
Tip: for "a new user was created", useorgscope. The event belongs to the *new* user, not to you, so auser-scoped webhook would never fire for it.
If you ask for a scope you are not allowed to create, the API answers success: false with the code ACCESS_DENIED.
Who Receives Database, Project, Task And Scheduled-Job Events
For the fourteen database, project, task and scheduled-job events, delivery follows who owns the item that changed — not who made the change.
Your webhook's scope | It receives the event when |
user | You own the database, project, task or scheduled job the event is about |
org | The item is shared at organisation level and belongs to your organisation |
system | Always — every organisation, every item |
Two consequences worth knowing:
- An item that is private to one user never reaches an organisation webhook.
- If Sapience cannot tell whose item an event concerns, only
systemwebhooks receive it.
Every one of these events carries three extra fields that describe the item: owner_user_id, object_scope and object_org_id. Use them to confirm which item a delivery concerns. For a database event the item is the database that holds the row, table or form. A scheduled job that belongs to the system itself has no owner (owner_user_id is null) and is delivered to system webhooks only.
What Happens Behind the Scenes
- You register a webhook for an eligible event and give it a target URL (which must use
https://), an HTTP method (POSTorGET) and an authentication type.
- Sapience checks the URL. Addresses that point inside private networks (for example
localhost,10.x.x.xor cloud metadata addresses) are refused withWEBHOOK_TARGET_URL_REJECTED.
- Sapience returns a signing secret — once. It starts with
whsec_. Store it safely; it is never shown again, and listing your webhooks never reveals it.
- The event happens (for example, someone signs up to your organisation, or a task changes status).
- Sapience calls every matching live webhook, in addition to everything else that event normally triggers (such as welcome emails). Each call carries your authentication header and a signature you can verify.
- If your endpoint is briefly down, Sapience retries: up to 3 attempts in total, about 1 second and then 3 seconds apart, and only for network errors,
5xxresponses or429 Too Many Requests. A4xxanswer such as400is treated as final. Redirects are never followed.
- Every delivery is recorded in Sapience's activity log as succeeded or failed. Secrets are never written to the log.
The Four API Calls
What you want | Call | TypeScript SDK method |
See which events you can subscribe to | GET /api/v2/webhooks/eligible-events | client.v2.webhooks.retrieveEligibleEvents() |
Register a webhook | POST /api/v2/webhooks/create | client.v2.webhooks.create({...}) |
List your webhooks | GET /api/v2/webhooks/list | client.v2.webhooks.retrieveList() |
Delete a webhook | DELETE /api/v2/webhooks/{webhook_uid} | client.v2.webhooks.delete(uid) |
Every response has the usual Sapience shape: { success, data, message, status_code }. Always check success before using data.
Sample TypeScript Code
The examples use the Sapience TypeScript SDK (mba-ai-studio). Pass your Sapience access token as apiKey (the SDK sends it as Authorization: Bearer …), and your Sapience API address as baseURL.
Set up the client
import MbaAIStudio from 'mba-ai-studio';
const client = new MbaAIStudio({
apiKey: process.env.SAPIENCE_TOKEN, // your Sapience access token
baseURL: process.env.SAPIENCE_API_BASE_URL, // your Sapience API address
});1. List the eligible events
const events = await client.v2.webhooks.retrieveEligibleEvents();
if (!events.success) throw new Error(events.message ?? 'could not list events');
for (const e of events.data ?? []) {
console.log(e.event, '-', e.description);
console.log(' payload fields:', e.payload_fields.join(', '));
}
// user_created - A new user account was created
// payload fields: user_id, username, email, org_id, org_name, role, is_anonymous, org_id_slug
// project_status_changed - A project changed status (completed, archived, reactivated)
// payload fields: project_uid, project_name, old_status, new_status, ...
// ... eleven events in all2. Register a webhook
const created = await client.v2.webhooks.create({
trigger_event: 'user_created',
scope: 'org', // org admins; 'system' = superusers only
webhook_comment: 'Add new users to our CRM',
webhook_target_details: {
target_url: 'https://hooks.example.com/sapience/new-user',
method: 'POST',
auth_type: { type: 'bearer', bearer_key: process.env.MY_ENDPOINT_TOKEN! },
},
});
if (!created.success || !created.data) {
// e.g. WEBHOOK_EVENT_NOT_ELIGIBLE, WEBHOOK_TARGET_URL_REJECTED, ACCESS_DENIED
throw new Error(created.message ?? 'webhook not created');
}
console.log('webhook id:', created.data.uid); // "whk-..."
// Shown ONCE — store it in your secret manager now.
const signingSecret = created.data.signing_secret; // "whsec_..."The four authentication types you can use for auth_type:
{ type: 'none' }
{ type: 'bearer', bearer_key: '...' } // sends Authorization: Bearer ...
{ type: 'api_key', api_key: '...' } // sends X-API-Key: ...
{ type: 'basic', username: '...', password: '...' } // sends Authorization: Basic ...Secrets (bearer_key, api_key, password) must be at least 8 characters long.
3. List your webhooks
const list = await client.v2.webhooks.retrieveList();
if (!list.success) throw new Error(list.message ?? 'could not list webhooks');
for (const w of list.data ?? []) {
console.log(w.uid, w.scope, w.trigger_event, w.method, w.target_url, w.auth_hint);
}The list shows only a short hint of each secret (like …a1b2), never the secret itself.
4. Delete a webhook
const deleted = await client.v2.webhooks.delete('whk-0f7c...');
if (!deleted.success) {
// WEBHOOK_NOT_FOUND if the id is unknown or not visible to you
throw new Error(deleted.message ?? 'delete failed');
}A deleted webhook stops firing immediately.
5. Receive and verify a delivery (Node.js / Express)
Each delivery is signed: X-Sapience-Signature is v1= followed by the HMAC-SHA256 of "<X-Sapience-Timestamp>.<raw body>", keyed with your signing secret. Verify against the raw body bytes, before any JSON parsing.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = express();
const SIGNING_SECRET = process.env.SAPIENCE_WEBHOOK_SECRET!; // the whsec_... value
app.post('/sapience/new-user', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.header('X-Sapience-Timestamp') ?? '';
const signature = req.header('X-Sapience-Signature') ?? '';
const body = req.body as Buffer;
const expected = 'v1=' + createHmac('sha256', SIGNING_SECRET)
.update(`${timestamp}.`)
.update(body)
.digest('hex');
const ok = expected.length === signature.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!ok) return res.status(401).send('bad signature');
// Optional: reject old deliveries (replay protection), e.g. older than 5 minutes.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(401).send('stale');
}
const event = JSON.parse(body.toString('utf8'));
console.log('new user:', event.data.email, 'in org', event.data.org_name);
res.sendStatus(200); // any 2xx = delivered
});
app.listen(3000);For GET webhooks there is no body: the same JSON arrives in the payload query parameter, and the signature is computed over that string.
6. Subscribe to task events and react to each one
Register one webhook per event, then verify the signature exactly as in step 5 and branch on the event's type.
for (const trigger_event of ['task_status_changed', 'task_due_date_reached']) {
const res = await client.v2.webhooks.create({
trigger_event,
scope: 'user', // events for tasks you own
webhook_comment: `Mirror ${trigger_event} into our tracker`,
webhook_target_details: {
target_url: 'https://hooks.example.com/sapience/tasks',
method: 'POST',
auth_type: { type: 'bearer', bearer_key: process.env.MY_ENDPOINT_TOKEN! },
},
});
if (!res.success) throw new Error(res.message ?? `could not register ${trigger_event}`);
// store res.data.signing_secret for this webhook
}// Inside your verified handler, after JSON.parse:
switch (event.type) {
case 'task_status_changed':
if (event.data.new_status === 'completed') markDone(event.data.task_uid);
break;
case 'task_due_date_reached':
remind(event.data.assigned_to_username, event.data.task_name);
break;
case 'scheduled_job_finished':
if (event.data.status === 'failed') alertOps(event.data.error_message);
else saveAnswer(event.data.job_uid, event.data.output_text);
break;
}What Your Endpoint Receives
Headers on every call:
Header | Value |
User-Agent | Sapience-Webhooks/1 |
X-Sapience-Event | the event name, e.g. user_created |
X-Sapience-Event-Id | a stable id for the event — use it to ignore duplicates |
X-Sapience-Webhook-Id | the webhook's whk-… id |
X-Sapience-Timestamp | Unix seconds when the call was signed |
X-Sapience-Signature | v1=<hex HMAC-SHA256> |
your auth header | per the webhook's auth_type |
The body (for POST):
{"id":"...","type":"user_created","created_at_utc":"2026-09-25T01:02:03Z","webhook_uid":"whk-...","api_version":"2026-09-25","data":{"user_id":123,"username":"jane","email":"jane@example.com","org_id":42,"org_name":"Example Co","role":"user","is_anonymous":false,"org_id_slug":null}}What Each New Event Carries
The data object of each event holds only the fields listed here. Any field can be null when its value is not known. Date and time values are ISO-8601 text.
Event | Fields in data |
project_status_changed | project_uid, project_name, old_status, new_status, changed_by_user_id, changed_by_username, owner_username |
project_file_uploaded | project_uid, project_name, file_uid, file_name, container_uid (the project itself, or the folder inside it that received the file), added_by_user_id, added_by_username |
task_created | task_uid, task_name, task_status, due_date, assigned_to_username, assigned_to_user_id, creator_user_id, creator_username, project_uid, project_name, parent_uid |
task_due_date_reached | task_uid, task_name, task_status, due_date, assigned_to_username, assigned_to_user_id, project_uid, project_name |
task_status_changed | task_uid, task_name, old_status, new_status, changed_by_user_id, changed_by_username, assigned_to_username, project_uid, project_name |
task_assignee_changed | task_uid, task_name, old_assignee, new_assignee, new_assignee_user_id, assigned_by_user_id, assigned_by_username, project_uid, project_name |
scheduled_job_finished | job_uid, job_name, job_type, status, started_at_utc, finished_at_utc, duration_seconds, result_summary, output_text, output_truncated, conversation_uid, agent_uid, error_message, error_type |
All seven also carry owner_user_id, object_scope and object_org_id (see Who Receives Database, Project, Task And Scheduled-Job Events). The seven database events carry the same three fields and are described under Database Webhooks: Tables, Conditions And Views below.
Status values. Tasks use todo, in_progress, completed, blocked, needs review, next and wont_do. Projects use active, completed and archived.
Scheduled jobs and their results
scheduled_job_finished fires once per run, whether the job completed or failed.
statusiscompletedorfailed.duration_secondsis a number of seconds.
- Agent query jobs (
job_typeofrun-agent-query) also carry the agent's answer inoutput_text, up to 16,000 characters. If the answer was longer, it is cut andoutput_truncatedistrue.conversation_uidandagent_uidpoint at the conversation and agent that produced it. For every other kind of job these four fields arenull.
- Failed runs fill in
error_messageanderror_type; the output fields staynull.
- Email addresses are removed from
result_summaryanderror_message— any email-shaped text there arrives as[redacted-email].output_textis the agent's own answer and is delivered exactly as written.
Example: a task was completed
{"id":"...","type":"task_status_changed","created_at_utc":"2026-09-27T09:18:00Z","webhook_uid":"whk-...","api_version":"2026-09-25","data":{"task_uid":"task-1","task_name":"Ship release notes","old_status":"in_progress","new_status":"completed","changed_by_user_id":6234,"changed_by_username":"mba.ken","assigned_to_username":"mba.ken","project_uid":"proj-1","project_name":"Q4 Roadmap","owner_user_id":6234,"object_scope":"user","object_org_id":null}}Example: a scheduled agent job finished
{"id":"...","type":"scheduled_job_finished","created_at_utc":"2026-09-27T09:20:00Z","webhook_uid":"whk-...","api_version":"2026-09-25","data":{"job_uid":"schedjob-abc","job_name":"Morning briefing","job_type":"run-agent-query","status":"completed","started_at_utc":"2026-09-27T09:19:55Z","finished_at_utc":"2026-09-27T09:20:00Z","duration_seconds":4.8,"result_summary":"ok","output_text":"Here is your morning briefing...","output_truncated":false,"conversation_uid":"conv-1","agent_uid":"agent-1","error_message":null,"error_type":null,"owner_user_id":null,"object_scope":"system","object_org_id":null}}Database Webhooks: Tables, Conditions And Views
Seven events report on your Sapience databases — their rows, their columns and their public forms. Left wide open, a webhook fires for every database that falls inside its scope. You can instead narrow it to one table and, for some events, to the rows that match conditions or to one saved view.
Event | Fires when | You can narrow it with |
database_row_added | A row is added — created in the table, or linked in | table_uid, and optional conditions |
database_row_updated | A cell in an existing row changes | table_uid, and optional conditions |
database_row_entered_view | A row enters a saved view | table_uid and view_uid (both required) |
database_row_removed | A row is removed (unlinked) from the table | table_uid only |
database_row_changed | A row is saved — a cell, a move or a rename | table_uid only |
database_schema_changed | The table's columns change | table_uid only |
database_form_submitted | A public form that writes into the table is submitted | table_uid only |
The database_trigger object
Add database_trigger next to trigger_event, scope and webhook_target_details in the register call. It is accepted only on the seven database events. For database_row_removed, database_row_changed, database_schema_changed and database_form_submitted it names table_uid only — conditions or a view are refused.
Field | Required? | What it is |
table_uid | Yes | The table to watch (it starts with dbt-). One webhook watches one table — register a second webhook for a second table. |
conditions | No — row_added and row_updated only | A rule, or a group of rules, that the row must satisfy. See Conditions below. |
view_uid | Yes for row_entered_view; not allowed for any other event | The saved view to watch (it starts with dbv-). |
Use IDs, not names. Tables, views and columns are referenced by ID. In the Sapience app, open a table's tab menu (…) and choose Copy table ID (or Copy database ID); open a column's header menu and choose Copy column key; use a row's context menu for Copy row ID; and right-click a saved view in the sidebar for Copy view ID.
Conditions
A condition tests one column: { "kind": "condition", "column": "status", "cmp": "eq", "value": "Done" }. Join several with { "kind": "and", "children": [ … ] } (or "or"), and flip one with { "kind": "not", "child": { … } }. column is the column's key — the value you get from Copy column key.
What you would say in plain words, and what you send:
In words | What you send | Notes |
is | "cmp": "eq" | Exact match. Text is case-sensitive. |
is not | "cmp": "neq" | Also matches a row whose cell is empty. |
contains | "cmp": "contains" | Text: a case-insensitive match anywhere in the cell. Multi-select: one of the selected options equals the value. |
does not contain | "kind": "not" around a contains | Does not match a row whose cell is empty — use neq if you need that. |
is empty | "cmp": "is_empty" | No value needed. |
is not empty | "cmp": "is_not_empty" | No value needed. |
is one of | "cmp": "in" | value is a list. |
greater / less than | "cmp": "gt", "gte", "lt" or "lte" | Ordering comparisons. Sapience checks the column type when you register and refuses a combination it cannot evaluate. |
Reacting to a status change. Register database_row_updated with a condition on the status column, for example status eq Done. With conditions, a row_updated webhook fires only when a row starts matching — it matched after the edit and did not match before — so editing a row that is already Done does not fire it again. For row_added, conditions are checked against the new row's cells.
Reference (link) columns. For a column that links to rows in another table, use eq, in or is_empty, and give the linked row's ID (Copy row ID) as the value. Lookup columns cannot be used in conditions.
Conditions are checked when you register the webhook. A column that does not exist, an operator or value the column cannot take, a row-level field such as position or created_at, or a lookup column is refused with DATABASE_UNSUPPORTED_SORT_OR_FILTER — so a webhook that could never fire is not created.
Register a database webhook
Use the same client.v2.webhooks.create() call as in step 2 and add database_trigger next to webhook_target_details. The access token, the authentication types and the one-time signing_secret all work exactly as before.
const created = await client.v2.webhooks.create({
trigger_event: 'database_row_updated',
scope: 'user', // databases you own; 'org' (admins) for organisation-level ones
webhook_comment: 'Tell our tracker when a task is marked Done',
webhook_target_details: {
target_url: 'https://hooks.example.com/sapience/rows',
method: 'POST',
auth_type: { type: 'bearer', bearer_key: process.env.MY_ENDPOINT_TOKEN! },
},
database_trigger: {
table_uid: 'dbt-…', // from "Copy table ID"
conditions: { kind: 'condition', column: 'status', cmp: 'eq', value: 'Done' },
},
});
if (!created.success || !created.data) throw new Error(created.message ?? 'webhook not created');
const signingSecret = created.data.signing_secret; // shown ONCE — store it nowTo fire when a row enters a saved view, change those two lines:
trigger_event: 'database_row_entered_view',
database_trigger: { table_uid: 'dbt-…', view_uid: 'dbv-…' }, // view_uid is required for this eventTo combine several conditions, group them with and (or or):
database_trigger: {
table_uid: 'dbt-…',
conditions: {
kind: 'and',
children: [
{ kind: 'condition', column: 'status', cmp: 'eq', value: 'Open' },
{ kind: 'condition', column: 'priority', cmp: 'in', value: ['High', 'Urgent'] },
],
},
},What a database delivery looks like
Headers, signing, retries and the delivery log are exactly the same as for every other event — verify the signature as in step 5. The data object depends on the event:
Event | Fields in data |
database_row_added | database_uid, table_uid, row_uid, position, created_at_utc, added_by_user_id, row_kind (created or linked), data (the cells of a created row, keyed by column key; null for a linked row) and linked_resource (the object that was linked in, for linked rows) |
database_row_updated | database_uid, table_uid, row_uid, changed_keys, before and after — before and after hold only the cells that changed |
database_row_entered_view | database_uid, table_uid, view_uid, row_uid — IDs only; look the row up if you need its cells |
database_row_removed | database_uid, table_uid, row_uid (the removed row), target_uid and target_type (the object the row pointed at — it still exists; only the row is gone), removed_by_user_id |
database_row_changed | database_uid, table_uid, row_uid, target_uid, changed_keys (the column keys that were saved — never their values), position (the new position when the row moved, otherwise null), display_name_changed (true when the row was renamed), needs_resolve (true when a saved column is a reference or file column), changed_by_user_id |
database_schema_changed | database_uid, table_uid, updated_at_utc (when the table's columns changed), op_kinds (what happened, in order: add_column, patch_column, delete_column or reorder_columns), column_keys (every column the change touched), changed_by_user_id |
database_form_submitted | form_uid, form_title, database_uid, table_uid, row_uid (the row the response became), file_count, submitted_at_utc, and submitted_values — a list of { "label": …, "value": … } objects, one per answered question in form order (a file question carries the file names). The form owner's email address is never included |
All seven also carry owner_user_id, object_scope and object_org_id. A reference cell arrives as a list of small objects naming each linked row (row_uid); the linked object's own uid and type are filled in only when it lives in the same database and scope, and are null otherwise.
Example: a task row was marked Done
{"id":"...","type":"database_row_updated","created_at_utc":"2026-10-03T01:00:00Z","webhook_uid":"whk-...","api_version":"2026-09-25","data":{"database_uid":"db-...","table_uid":"dbt-...","row_uid":"...","changed_keys":["status"],"before":{"status":"Open"},"after":{"status":"Done"},"owner_user_id":7,"object_scope":"user","object_org_id":2}}Example: someone submitted a public form
{"id":"...","type":"database_form_submitted","created_at_utc":"2026-10-14T01:00:00Z","webhook_uid":"whk-...","api_version":"2026-09-25","data":{"form_uid":"...","form_title":"Event sign-up","database_uid":"db-...","table_uid":"dbt-...","row_uid":"...","file_count":0,"submitted_at_utc":"2026-10-14T01:00:00Z","submitted_values":[{"label":"Name","value":"Jane Citizen"},{"label":"Company","value":"Example Co"}],"owner_user_id":7,"object_scope":"user","object_org_id":2}}Good To Know
- Due dates are checked every 15 minutes.
task_due_date_reachedcan arrive up to about 15 minutes after the due moment. It fires once per due date: change a task's due date and it fires again for the new date. Completed and won't-do tasks never fire, and a task whose due moment passed more than 24 hours before the check is not reported.
- Delivery follows the owner. Nobody else gets a separate copy: a task assigned to a colleague reaches your
userwebhook only if you own the task, and a project's collaborators are not a delivery target. To follow a whole team's work, an organisation admin can register anorgwebhook.
- Some file moves do not send the upload event. Adding a file to a project (one file or many at once) sends
project_file_uploaded. Moving or copying files into a project in bulk does not.
- A folder-only share does not send the upload event. If someone can reach only a folder inside a project — with no access to the project itself — their upload succeeds but no
project_file_uploadedis sent.
- Usernames appear as stored. Fields such as
changed_by_username,assigned_to_username,old_assigneeandnew_assigneecarry usernames exactly as they are stored in Sapience.
- Database updates fire only on real changes.
database_row_updatedis sent only when a cell value actually changed. Saving the same value, moving a row or renaming it sends nothing.
- Every save sends
database_row_changed. It is sent for every saved edit to a row — a cell (even one saved with the same value), a move or a rename — and names the columns, never the values. Use it to know that a row changed; usedatabase_row_updatedto know what changed.
- Conditions and views react to the moment a row starts matching. With conditions,
row_updatedfires when the row matches after the edit and did not before.row_entered_viewworks the same way for a saved view: leaving a view never fires, and a view with no filter only ever receives newly created rows.
- Linked rows and
row_addedconditions. A row that is linked into a table, rather than created in it, carries no cell data, so arow_addedwebhook that hasconditionsdoes not fire for it today. Without conditions it does fire (row_kindislinked).
- A condition that can no longer be checked stops that webhook. If a column used in
conditionsis deleted or changes type, or the table cannot be read, Sapience delivers nothing for that webhook rather than guess. Webhooks without conditions keep firing. There is no edit call: delete the webhook and register a new one.
- Who made a database change.
database_row_added,database_row_removed,database_row_changedanddatabase_schema_changedcarry the user's ID (added_by_user_id,removed_by_user_idorchanged_by_user_id) — an ID, not a username.database_row_updatedanddatabase_row_entered_viewdo not say who made the change.
- Form submissions are anonymous.
database_form_submittedcarries what was submitted, never who submitted it, and never the form owner's email address.
Errors You Might See
system_code | Meaning | What to do |
WEBHOOK_EVENT_NOT_ELIGIBLE (1106) | The event can't have webhooks | Pick one from eligible-events |
WEBHOOK_TARGET_URL_REJECTED (1107) | The URL isn't https, doesn't resolve, or points at a private address | Use a public https:// URL |
WEBHOOK_NOT_FOUND (1108) | Unknown webhook id, or one you can't see | Check the id with list |
ACCESS_DENIED (403) | You can't create that scope | Use a scope your role allows |
DATABASE_TABLE_NOT_FOUND (2610) | The table_uid does not exist, or is not a table | Copy the table's ID again |
DATABASE_NOT_FOUND (1093) | The database that holds the table can't be found | Check the table still exists |
DATABASE_ACCESS_DENIED (1094) | You can't read that database, so you can't watch it | Ask its owner for access |
DATABASE_UNSUPPORTED_SORT_OR_FILTER (1100) | A condition names an unknown column, an operator or value the column can't take, a row-level field, or a lookup column | Check the column keys and operators under Conditions |
DATABASE_VIEW_MISMATCH (1099) | The view_uid is not a view of that table, or the view's own filter can't be used | Copy the view ID from the same table |
system_code arrives as a number (shown in brackets). A database_trigger that does not fit the event is refused as an invalid request (HTTP 422); the reason is the type of the entry in data.errors: database_trigger_not_allowed_for_event, view_uid_not_allowed_for_event, conditions_not_allowed_for_event or view_uid_required.
Summary
Outbound webhooks push Sapience events straight to your systems as they happen — new users, database rows being added, updated, changed, removed or entering a saved view (optionally narrowed by field conditions), table column changes, public form submissions with their answers, project status changes and file uploads, task creation, due dates, status and assignee changes, and the finished result of scheduled jobs. Register a webhook for each event you care about, store the one-time signing secret, and verify each delivery's signature. Sapience handles authentication, retries and an activity log for every call.