Webhook Event Payloads
The structure of every webhook event Simply360 delivers: the common envelope plus per-event-type payload fields.
Overview
Each webhook delivery is an HTTP POST with a signed JSON body. All current webhook events use the same envelope; the data object varies by event type.
For setup, subscription management, signing-secret verification, and delivery logs, see the Webhooks reference.
Common Envelope
{
"eventId": "EVNT-1234-ABCD",
"eventType": "DATA_RECORD_CREATED",
"timestamp": "2026-06-28T14:23:45.000Z",
"teamId": "1042",
"data": {
"dataCollectionId": "DCOL-1234-ABCD",
"dataCollectionSimplyId": "DCOL-1234-ABCD",
"dataRecordSimplyId": "DREC-5678-EFGH"
},
"metadata": {
"timestamp": "2026-06-28T14:23:44.500Z",
"source": "API_CREATE"
}
}
| Field | Type | Description |
|---|---|---|
eventId | string | Stable identifier for the processed event. Record events use a CHAR-14 code. Wizard completions use wizard:<dataWizardInstanceSimplyId>:completion:v1. Use the complete value to deduplicate retries. |
eventType | string | One of the documented event type strings. |
timestamp | string | ISO 8601 timestamp generated when the delivery body is built. Stable across retries of the same delivery. |
teamId | string | The team's internal numeric identifier, serialized as a string, on legacy non-Wizard event envelopes. It is not the team's simplyId and is omitted from WIZARD_COMPLETED. |
teamSimplyId | string | The team's public Simply ID. Present on WIZARD_COMPLETED instead of teamId. |
data | object | Event-specific payload. |
metadata | object | Producer metadata: the original event timestamp and a source trace tag (e.g. API_CREATE, API_BATCH_UPDATE, MCP_CREATE, WIZARD, SSP_SYNC). |
Headers on Every Delivery
| Header | Description |
|---|---|
Content-Type | application/json |
X-Simply360-Event-Id | Same value as eventId in the body. |
X-Simply360-Event-Type | Same value as eventType in the body. |
X-Simply360-Timestamp | Same value as timestamp in the body. |
X-S360-Signature | Hex-encoded HMAC-SHA-256 of the raw request body using the subscription secret. This is the default header name; a subscription can configure a different one. |
Event Payloads
DATA_RECORD_CREATED
Fired when a data record is created through supported record-write paths.
{
"eventType": "DATA_RECORD_CREATED",
"data": {
"dataCollectionId": "DCOL-1234-ABCD",
"dataCollectionSimplyId": "DCOL-1234-ABCD",
"dataRecordSimplyId": "DREC-5678-EFGH"
}
}
DATA_RECORD_UPDATED
Fired when a data record is updated through supported record-write paths.
{
"eventType": "DATA_RECORD_UPDATED",
"data": {
"dataCollectionId": "DCOL-1234-ABCD",
"dataCollectionSimplyId": "DCOL-1234-ABCD",
"dataRecordSimplyId": "DREC-5678-EFGH"
}
}
DATA_RECORD_DELETED
Fired when a data record is deleted through supported record-write paths.
{
"eventType": "DATA_RECORD_DELETED",
"data": {
"dataCollectionId": "DCOL-1234-ABCD",
"dataCollectionSimplyId": "DCOL-1234-ABCD",
"dataRecordSimplyId": "DREC-5678-EFGH"
}
}
DATA_RECORDS_BULK_SYNCED
Fired once for a bulk write or sync operation instead of emitting one event per record. Use the collection ID and aggregate counts to decide whether to fetch changed records from the API.
{
"eventType": "DATA_RECORDS_BULK_SYNCED",
"data": {
"dataCollectionId": "DCOL-1234-ABCD",
"dataCollectionSimplyId": "DCOL-1234-ABCD",
"recordCount": 1250,
"changeBreakdown": {
"created": 800,
"updated": 430,
"deleted": 20
},
"triggerSource": "SSP_SYNC",
"syncStartedAt": "2026-06-28T14:00:00.000Z",
"syncJobId": "SJOB-1234-ABCD"
}
}
WIZARD_COMPLETED
Fired when a visitor completes a public Data Wizard. The payload carries the wizard identifiers, the submitted values, and the resulting record when the wizard created or updated one. Subscriptions can be filtered with filterCriteria.dataWizardSimplyId (and dataWizardStepSimplyId). This event uses the public teamSimplyId envelope field instead of the legacy numeric teamId field.
{
"eventId": "wizard:INST-1234-ABCD:completion:v1",
"eventType": "WIZARD_COMPLETED",
"timestamp": "2026-06-28T14:23:44.500Z",
"teamSimplyId": "TEAM-1234-ABCD",
"data": {
"completionEventKey": "wizard:INST-1234-ABCD:completion:v1",
"dataWizardSimplyId": "WIZD-1234-ABCD",
"dataWizardInstanceSimplyId": "INST-1234-ABCD",
"dataWizardStepSimplyId": "WSTP-1234-ABCD",
"wizardValues": { "First Name": "Ada", "Email": "ada@example.com" },
"tempVariables": {},
"outputValues": {},
"dataRecordSimplyId": "DREC-5678-EFGH"
},
"metadata": {
"timestamp": "2026-06-28T14:23:44.500Z",
"source": "PublicWizardService",
"clientIp": "203.0.113.10"
}
}
Payload Field Notes
dataCollectionIdis a legacy compatibility key whose value is a DataCollectionsimplyId. New integrations should preferdataCollectionSimplyIdwhen reading payloads.dataRecordSimplyIdis present on per-record events, and onWIZARD_COMPLETEDwhen the wizard produced a record.DATA_RECORDS_BULK_SYNCEDis a summary event. It does not include individual record IDs.WIZARD_COMPLETEDexposes only public identifiers: its envelope usesteamSimplyId, and itseventIdanddata.completionEventKeyare the same stable completion key.- The
metadata.sourcevalue identifies the producer path, such as API, MCP, wizard task execution, or integration sync.
Idempotency & Retry Handling
General webhook deliveries are at-least-once: a failed delivery is retried up to the subscription's retryCount (default 3) with exponential backoff starting at retryDelaySeconds (default 60). The body bytes and signature are identical across retries. Store the complete eventId before processing the payload and ignore duplicates. WIZARD_COMPLETED uses the narrower durable-claim contract below.
For WIZARD_COMPLETED, Simply360 durably claims one delivery per subscription and completion key before making the HTTP request. Queue redelivery does not create a second HTTP request when that claim is already pending, success, or failed. A pending claim is intentionally not replayed automatically because the remote endpoint may have accepted the request before Simply360 recorded its response; an operator must reconcile that ambiguous delivery using the stable eventId.
app.post('/webhooks/simply360', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.header('x-s360-signature') ?? '';
const rawBody = (req.body as Buffer).toString('utf8');
if (!verifyWebhook(rawBody, signature, secret)) {
return res.status(401).end();
}
const event = JSON.parse(rawBody);
const inserted = await db.webhookEvents.insertIfAbsent({
id: event.eventId,
type: event.eventType,
payload: event,
});
if (!inserted) return res.status(200).end();
await processEvent(event);
res.status(200).end();
});
Versioning
Webhook payloads may receive additive fields over time. Treat unknown fields as safe to ignore, and do not assume data contains full record field values. Fetch the record by dataRecordSimplyId when your handler needs the current record state.