Help Agent

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 to task_status_changed and check for new_status equal to completed.
Database events: all seven database events can be narrowed to one table; database_row_added and database_row_updated also to rows that match conditions, and database_row_entered_view to 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", use org scope. The event belongs to the *new* user, not to you, so a user-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 system webhooks 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

  1. You register a webhook for an eligible event and give it a target URL (which must use https://), an HTTP method (POST or GET) and an authentication type.
  1. Sapience checks the URL. Addresses that point inside private networks (for example localhost, 10.x.x.x or cloud metadata addresses) are refused with WEBHOOK_TARGET_URL_REJECTED.
  1. 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.
  1. The event happens (for example, someone signs up to your organisation, or a task changes status).
  1. 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.
  1. 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, 5xx responses or 429 Too Many Requests. A 4xx answer such as 400 is treated as final. Redirects are never followed.
  1. 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 all

2. 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.

  • status is completed or failed. duration_seconds is a number of seconds.
  • Agent query jobs (job_type of run-agent-query) also carry the agent's answer in output_text, up to 16,000 characters. If the answer was longer, it is cut and output_truncated is true. conversation_uid and agent_uid point at the conversation and agent that produced it. For every other kind of job these four fields are null.
  • Failed runs fill in error_message and error_type; the output fields stay null.
  • Email addresses are removed from result_summary and error_message — any email-shaped text there arrives as [redacted-email]. output_text is 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 now

To 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 event

To 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_reached can 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 user webhook 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 an org webhook.
  • 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_uploaded is sent.
  • Usernames appear as stored. Fields such as changed_by_username, assigned_to_username, old_assignee and new_assignee carry usernames exactly as they are stored in Sapience.
  • Database updates fire only on real changes. database_row_updated is 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; use database_row_updated to know what changed.
  • Conditions and views react to the moment a row starts matching. With conditions, row_updated fires when the row matches after the edit and did not before. row_entered_view works 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_added conditions. A row that is linked into a table, rather than created in it, carries no cell data, so a row_added webhook that has conditions does not fire for it today. Without conditions it does fire (row_kind is linked).
  • A condition that can no longer be checked stops that webhook. If a column used in conditions is 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_changed and database_schema_changed carry the user's ID (added_by_user_id, removed_by_user_id or changed_by_user_id) — an ID, not a username. database_row_updated and database_row_entered_view do not say who made the change.
  • Form submissions are anonymous. database_form_submitted carries 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.

Did this answer your question?
😞
😐
🤩