Webhooks
Webhooks
Spektr can push events to an HTTPS endpoint of yours as they happen. In the product the mechanism is called export delivery and each subscription is a delivery rule — "webhook" in this document means the same thing.
Configure it under Settings → Developer → Export settings: set the endpoint, choose an authentication method (API key or HMAC), and add one or more delivery rules.
Request shape
Every delivery is a POST with Content-Type: application/json. The body is a batch — treat results as a list and process each entry independently, because one request may carry several events:
{
"results": [
{
"id": "<event id>",
"workspaceId": "<workspace id>",
"eventType": "process_expired",
"timestamp": 1731057600000,
"spektrId": "<customer id>",
"data": { "...": "event-specific payload" }
}
]
}Any 2xx response acknowledges the batch. Any non-2xx status, timeout, or connection error is treated as a failure and the batch is retried. Repeated consecutive failures are retried progressively less often, so delivery may not resume the moment a recovered endpoint comes back. Nothing is discarded while it is down — undelivered events accumulate and replay oldest-first. Record the event id and ignore ids you have already processed — the same event may legitimately arrive more than once.
For signature verification, see the HMAC guide.
Delivery rules
| Rule kind | Configured with | Receives |
|---|---|---|
| Per-process | one or more processes + event types | only events bound to one of those processes |
| Event-only | event types, no process | only events that are not bound to a process |
Each rule also has a delivery timing:
- On submit — delivered as soon as the event is recorded.
- End of process — held until the process run reaches a terminal status, then delivered.
Events
process_expired
process_expiredA client-facing process link — the tokenised end-client URL behind a form — reached the end of its 90-day life without being completed. Nothing was submitted; the invitation lapsed.
At the same moment the process run moves to the terminal status expired, the customer timeline gets a Process expired entry, and the end client visiting the link sees a terminal screen instead of the form.
This event is process-bound, so subscribe to it with a per-process rule; an event-only rule never receives it. Both delivery timings work.
data carries:
| Field | Type | Description |
|---|---|---|
contextId | string | The process run (execution) the link belonged to. |
processId | string | The process the run belongs to. |
processType | string | One of onboarding, loop, questionnaire — the process types that can hold a form. |
processName | string, optional | The process name at the time of expiry. |
linkExpiredAt | number | Unix timestamp in milliseconds at which the link itself expired. |
expiredAt | number | Unix timestamp in milliseconds at which Spektr detected the expiry. |
linkExpiredAt and expiredAt are two different instants and both are part of the contract. The first is when the link died, the second is when the platform noticed — normally within a minute. Use linkExpiredAt for anything user-facing; use expiredAt only to reconcile against delivery time.
Volume. Links are usually issued in campaigns, so they also lapse in campaigns 90 days later. Expiries are detected in bounded batches, one batch per minute per workspace, so a large campaign drains over several minutes instead of arriving as a single burst. The same cap bounds the other consumer of this event: an orchestration process triggered by process_expired starts one full process run per expiry, so the batch size is what limits how many of those can start per minute. Contact us if you need the batch size raised for a workspace.
Scope. Only links held by form-bearing process types (onboarding, loop, questionnaire) are reported. A run that is not waiting on the end client — for example one already picked up for processing — is not reported by this event.
form_submit
form_submitA process run processed a submitted form step. The event carries the submitted and dataset field values of the customer, and lists the related entities — owners, directors, signatories — added in that form, so you can contact them without a second lookup.
This event is process-bound, so subscribe to it with a per-process rule; an event-only rule never receives it. Both delivery timings work.
data carries the submitted and dataset field values, such as company_name, next to these fields:
| Field | Type | Description |
|---|---|---|
stepType | string | Type of the form step, for example form. |
stepId | string | The form step that was submitted. |
formName | string, optional | Name of the form. |
contextId | string | The process run (execution) the form belongs to. |
processId | string | The process the run belongs to. |
processName | string | The process name at the time of submission. |
processType | string | Type of the process, for example onboarding or loop. |
reference | string, optional | Reference of the customer or run, when set. |
filledBy | string, optional | Who filled in the form, when known. |
relatedEntities | array | Related entities submitted in this form that have an email. Present on events recorded after this field was released. |
Each relatedEntities entry carries:
| Field | Type | Description |
|---|---|---|
spektrId | string | The customer record of the related entity. |
email | string | The email submitted for the related entity in this form. |
full_name | string, optional | Full name stored on the related entity. |
first_name | string, optional | First name stored on the related entity. |
last_name | string, optional | Last name stored on the related entity. |
company_name | string, optional | Company name stored on the related entity. |
company_number | string, optional | Company number stored on the related entity. |
A name field is present only when the related entity has a value for it; it is never null or an empty string. Entries follow the order in which the entities were submitted.
{
"company_name": "Cruz Holdings A/S",
"stepType": "form",
"stepId": "6ac66e80057fb0bac07e45e8",
"formName": "New form node",
"contextId": "6acd33ecd38e0f54965b3fc5",
"processId": "6ac4a6100b7c4d3cda91b015",
"processName": "Onb Process JJ 06/07",
"processType": "onboarding",
"reference": "A007-20261012-36FCZ",
"relatedEntities": [
{
"spektrId": "6acd33ecd38e0f54965b3fa1",
"email": "[email protected]",
"full_name": "Jane Cruz"
},
{
"spektrId": "6acd33ecd38e0f54965b3fa2",
"email": "[email protected]",
"first_name": "Lars",
"last_name": "Holm"
},
{
"spektrId": "6acd33ecd38e0f54965b3fa3",
"email": "[email protected]",
"company_name": "Cruz Invest ApS",
"company_number": "DK 41234567"
}
]
}Edge cases.
- A related entity submitted without an email is omitted, even if its customer record holds an email from an earlier run.
relatedEntitiesis an empty array when the form has no Entity or Role entities field, or when no submitted entity has an email.- Entities that share an email within one field are merged into one entry. The same email entered under both the Entity and the Role entities field creates two customer records, so it appears twice, once per
spektrId. - Entities removed in the form before submission are not listed.
Related API surface
The expired status is part of the execution status enum, so it can appear anywhere a process-run status is returned, including the public execution payload used by the client SDK. See the SDK documentation for the end-client behaviour.
Updated about 15 hours ago