Sync webhooks
Get notified at your own URL when records fail to sync, an error forces a sync to stop or a full read finishes. Events, payloads, issue ids, bursts and retries.
A sync webhook makes Stacksync send an HTTP POST to your URL when something happens on a sync. Each webhook listens to one notification type. To be told about syncing issues, use real_time_issue_notification.
Webhooks tell you that something happened and where. They are signals rather than a record of each issue, so read the issues themselves from the List sync issues API.
Keep these limits in mind when you build a receiver:
- Not one call per issue. An issue notification covers a failed batch of records and carries no issue id.
- No retries. Stacksync sends each notification once. If your endpoint fails, that notification is lost.
- No rate limit. A large failure can send many calls at once, in no guaranteed order.
To avoid missing an issue, treat the notification as a prompt to read the issues API, and also read that API on a schedule. See Track every issue.
Create a webhook
Webhooks are managed through the API: create, list, get, update and delete. These operations read and change a webhook's settings: its URL, notification type and API key. The events themselves are only ever sent to your URL, and no API lists past notifications.
curl -X POST "https://api.stacksync.com/v1/workspaces/{workspace_id}/bases/{base_id}/webhooks?region=besg" \
-H "Authorization: Bearer $STACKSYNC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/stacksync",
"notification_type": "real_time_issue_notification",
"api_key": "a-secret-your-endpoint-checks"
}'The URL must use https on a public internet host and answer an OPTIONS request within 2 seconds. Stacksync only runs this check when the webhook is created or updated. A sync can hold one webhook per notification type and URL.
Notification types
Choose a type with notification_type when you create the webhook. A webhook receives only its own type.
notification_type | What it tells you | Sync state | Extra fields |
|---|---|---|---|
real_time_issue_notification | Records could not be written to an app | Keeps running | app_id, internal_table_id |
base_activity | Stacksync stopped the sync | Stopped until someone switches it on | None |
backfill | A full read finished | Keeps running | None |
real_time_issue_notification
Use this type to learn about syncing issues, the same ones shown in the Issues dashboard.
What it means: Stacksync tried to insert, update or delete records in an app, and at least one could not be written, for example because of a validation rule or a datatype mismatch. Those records now have open issues and the sync keeps running.
When it is sent:
- Once for each batch with failures, for each table. See Bursts.
- Again when a record with an open issue fails again.
When it is not sent:
- Some issues are recorded without a notification, such as a record matched as a duplicate of one already synced, or an existing issue updated when the record is read again. Reading the issues API on a schedule picks these up.
- An issue is resolved. There is no notification for a retry that succeeds, a revert or an ignore.
What to do: the notification names the sync, app and table, but not the records or the error. Read them from the issues API, as described in Track every issue.
{
"notification": {
"created_time": "2026-11-05T14:02:11.482913",
"notification_type": "real_time_issue_notification",
"msg": "Some records had issues syncing on base: \"3f6c1d2e-8a4b-4c7d-9e21-5b6f7a8c9d01\" for the app: \"b71d04e5\", to view the issue(s) go to the issues tab in your workspace",
"base_id": "3f6c1d2e-8a4b-4c7d-9e21-5b6f7a8c9d01",
"app_id": "b71d04e5",
"internal_table_id": "4d20ffb3"
}
}base_activity
Use this type to learn that a sync has stopped. Despite its name, it does not report general activity on the sync.
What it means: Stacksync stopped the sync because it could not continue. No records sync until the cause is fixed and someone switches the sync on again.
When it is sent:
- An error Stacksync cannot recover from occurs.
- A temporary pause runs out of attempts to resume.
- The same error repeats too many times in a short period.
- The credentials of one of the sync's apps are found to be invalid. A single expired credential can send this more than once.
When it is not sent:
- A person switches or edits the sync.
- The sync fails to switch on.
- The sync is paused temporarily and resumes by itself.
- The schema of a connected app changes.
Rarely, a notification can follow a manual switch off, when a read in progress fails at that moment.
What to do: treat it as an alert. The payload does not name the app or the error, so show msg to a person or open the sync in Stacksync. msg is usually an HTML fragment, the same content as the alert email. For invalid credentials it is plain text.
{
"notification": {
"created_time": "2026-11-05T14:02:11.482913",
"notification_type": "base_activity",
"msg": "<p>Hi there,</p><p>Your Sync <strong>CRM to Postgres</strong> in workspace <strong>Acme</strong> is forced to be paused because of the following error...</p>",
"base_id": "3f6c1d2e-8a4b-4c7d-9e21-5b6f7a8c9d01"
}
}backfill
Use this type to learn that a sync has finished reading its existing records.
What it means: every full read of the sync has ended and the changes they found have been processed.
When it is sent: once, when all of these are true:
- The sync is running.
- Every full read process of the sync has ended.
- No changes from before the end of those reads are still waiting to be written.
Stacksync checks this periodically, so the notification can come some time after the read ends.
When it is not sent:
- A full read is still running on any table, or its changes are still being written.
- The sync is not running.
What to do: continue whatever was waiting for the initial data. The message says the process completed successfully, but that only means it ended: some records may still have failed. Check List sync issues if you need every record to be in place.
{
"notification": {
"created_time": "2026-11-05T14:02:11.482913",
"notification_type": "backfill",
"msg": "The back-fill process has been completed successfully for your base CRM to Postgres (id: 3f6c1d2e-8a4b-4c7d-9e21-5b6f7a8c9d01).",
"base_id": "3f6c1d2e-8a4b-4c7d-9e21-5b6f7a8c9d01"
}
}Payload
Every notification is a JSON object with a single notification key, sent with these headers:
Content-Type: application/json
Authorization: Bearer <api_key>api_key is the value you set on the webhook. Check it to confirm the call comes from Stacksync. Notifications are not signed, and the API returns this key when a webhook is read, so treat it as a secret.
| Field | Type | Present | Meaning |
|---|---|---|---|
created_time | string | Always | When the notification was built, ISO 8601 without a UTC offset. It is not an ordering guarantee |
notification_type | string | Always | One of the notification types |
msg | string | Always | Text for people. Do not parse it: its wording can change |
base_id | string | Always | The sync |
app_id | string | Issue notifications | The app the records could not be written to. Matches destination_app_id on the issue |
internal_table_id | string | Issue notifications | The table pair. Use it as table_id when you list issues |
The payload has no event id, no workspace id, no region and no count of the records affected. Keep the region of each sync on your side, because the issues API needs it.
Issue ids
Notifications do not contain issue ids. issue_id, the failed record, the changed columns and the error message are all returned by List sync issues.
A record has at most one open issue per app and table. If it fails again, the same issue is overwritten: it gets a new issue_id and a new last_modified_time and issue_message, and keeps its created_time. So:
issue_ididentifies one failure. Use it to avoid processing the same failure twice, and to resolve the issue.- To follow a record across repeated failures, use
base_id,destination_app_id,destination_table_idandsource_record_idordestination_record_id. A storedissue_idstops working once the record fails again.
Delivery and failures
Stacksync sends each notification once and does not look at your response.
| What happens on your side | Result |
|---|---|
| Your endpoint answers with any status | The response has no effect, and a 2xx is not treated differently from an error |
| It answers with an error, is unreachable or has an invalid certificate | The notification is lost. There is no retry and no replay |
| It is slow to answer | The sync does not wait for you, but answer quickly: a call still open when a Stacksync worker restarts is dropped |
| It redirects | The redirect is followed, and a 301 or 302 usually turns the POST into a GET with no body. Use the final URL |
| It fails repeatedly | The webhook stays active, and you are not alerted that deliveries are failing |
Failed deliveries never affect your data. The sync keeps running, and the issues stay in the Issues dashboard and the API until they are resolved. Delivery is best effort: a rare notification can also be lost on the Stacksync side, for example during a deployment.
Bursts
- One call per failed batch. Stacksync writes records in batches, and each batch with failures sends one call for its table. The number of calls depends on the batching rather than on the number of records, so 10,000 failed records send neither 10,000 calls nor a single one.
- No rate limit or grouping. Calls are not throttled or merged, and you cannot configure a limit. The limit of one alert email per sync every 6 hours applies to email only.
- Concurrent and unordered. Batches run in parallel and each call is sent independently, so calls can overlap and arrive out of order.
- Repeats. Records that keep failing send new notifications, each looking the same apart from
created_time. - Possibly early. The notification can arrive just before its issues can be read. Wait a few seconds before reading them.
If your endpoint receives more than it can handle, the calls it drops are not sent again, so keep the endpoint light.
Track every issue
To act on each issue reliably, including during bursts and outages:
- Answer immediately. Check the
Authorizationheader, putbase_idandinternal_table_idon a queue, and answer200before doing any other work. - Group the calls. Wait a few seconds and keep one pending read per
base_idandinternal_table_id, which turns a burst into a single read. - Read the issues. Call the API below and follow
pagination.next_pageuntil it is absent. Results are ordered by most recently modified first, so you can stop at issues older than your last read. Filter ondestination_app_idif you only want one app. - Skip what you have seen. Store each
issue_idyou have processed, because every new failure has a new one. - Read on a schedule too. Run the same read every few minutes for each sync, without
table_id. This picks up anything a lost notification missed.
curl "https://api.stacksync.com/v1/workspaces/{workspace_id}/issues?region=besg&base_id={base_id}&table_id={internal_table_id}&page_size=1000" \
-H "Authorization: Bearer $STACKSYNC_API_KEY"Each issue in the answer holds issue_id, the action tried, the error message, the changed columns and the record ids. Pagination and every field are described on the operation's page.
Issues dashboard
The issues dashboard helps you identify and fix all syncing issues potentially affecting your syncs. Your pipelines will never incur silent failures again.
Custom sync frequencies
Set your own data sync frequencies to match your workflow and system needs. Customize how often your data updates to stay current without unnecessary load.