See Stacksync in action with a solution architect. Book a demo ↗

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_typeWhat it tells youSync stateExtra fields
real_time_issue_notificationRecords could not be written to an appKeeps runningapp_id, internal_table_id
base_activityStacksync stopped the syncStopped until someone switches it onNone
backfillA full read finishedKeeps runningNone

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.

FieldTypePresentMeaning
created_timestringAlwaysWhen the notification was built, ISO 8601 without a UTC offset. It is not an ordering guarantee
notification_typestringAlwaysOne of the notification types
msgstringAlwaysText for people. Do not parse it: its wording can change
base_idstringAlwaysThe sync
app_idstringIssue notificationsThe app the records could not be written to. Matches destination_app_id on the issue
internal_table_idstringIssue notificationsThe 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_id identifies 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_id and source_record_id or destination_record_id. A stored issue_id stops 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 sideResult
Your endpoint answers with any statusThe 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 certificateThe notification is lost. There is no retry and no replay
It is slow to answerThe sync does not wait for you, but answer quickly: a call still open when a Stacksync worker restarts is dropped
It redirectsThe redirect is followed, and a 301 or 302 usually turns the POST into a GET with no body. Use the final URL
It fails repeatedlyThe 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:

  1. Answer immediately. Check the Authorization header, put base_id and internal_table_id on a queue, and answer 200 before doing any other work.
  2. Group the calls. Wait a few seconds and keep one pending read per base_id and internal_table_id, which turns a burst into a single read.
  3. Read the issues. Call the API below and follow pagination.next_page until it is absent. Results are ordered by most recently modified first, so you can stop at issues older than your last read. Filter on destination_app_id if you only want one app.
  4. Skip what you have seen. Store each issue_id you have processed, because every new failure has a new one.
  5. 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.

On this page