Ingestion Webhooks: Technical Guide
This guide is for the people who build the sender and the ingestion plan. It covers how Sprinto processes a webhook request, where the request body appears in your plan, authentication, duplicates, limits and troubleshooting.
To set up a webhook in Sprinto, see Create and Manage Webhooks.
How an event is processed
Sprinto checks each request to the webhook URL in this order:
Size. A body larger than the size limit is rejected with
413. See Limits.Webhook lookup. The webhook must exist and be active. If not, Sprinto returns
404.Rate limit. Requests over Rate Limit Per Minute are rejected with
429.Authentication. A request that fails the webhook's auth check is rejected with
401.Duplicate check. An event Sprinto has already received returns
200with{"status": "duplicate"}and does not run the plan.Event recorded. Sprinto saves the event and returns
202with{"status": "accepted", "eventPk": "..."}.
After the 202, Sprinto processes the event in the background:
Filter. If the webhook has a filter expression and it evaluates to false, the event is marked filtered and the plan does not run.
Plan run. Sprinto starts a run of the attached plan with the event's body.
A
202confirms receipt, not storage. An event's status becomes completed when it is handed to the plan engine. Whether its records were stored depends on the plan run. Check the plan run linked from the event.
Where the payload appears in your plan
The JSON body of the request is available in the plan as:
Only the body is passed to the plan. Request headers and query-string values are not available to plan steps.
There are no other webhook variables such as
trigger,eventorbody. Referring to one returns nothing, and it does not cause an error.Values from a Runtime Params Mapping are available as extra parameters next to
params.webhookPayload.
Write a plan for webhook data
A webhook plan usually has four steps:
A
transformstep that readsparams.webhookPayloadand returns a list of records.A
mapEntityTypestep that turns each record into the fields of your entity type.An
insertCustomEntitiesstep that stores the records.An
evaluateEntitiesstep that runs checks on the records from this run.
Example
The sender posts one server per request:
This plan stores it as a Server entity:
The square brackets in [$$.params.webhookPayload] make sure the next step always receives a list, whether the sender posts one object or an array.
Match the extract expression to your payload
One object: {"identifier": "a", ...}
[$$.params.webhookPayload]
An array: [{...}, {...}]
[$$.params.webhookPayload]
A list inside an object: {"items": [{...}, {...}]}
[$$.params.webhookPayload.items]
A record nested in an object: {"data": {"server": {...}}}
[$$.params.webhookPayload.data.server]
A wrong path doesn't fail the extract step. It returns an empty list, and the
mapEntityTypestep then reports every required field as missing. If you see that, check the extract path first.
Mapping rules
Output keys must match the entity type's field names exactly, including case and underscores.
Every record must include
name,identifierand every other field the entity type marks as Required.Don't map
lastSeenOnSource. Sprinto sets it when the record is stored.identifiermust be unique and stable for each record. A later record with the sameidentifierupdates the existing record instead of creating a new one.Send timestamps as ISO 8601 strings, for example
2026-09-09T00:00:00Z.Send booleans as JSON
trueorfalse. If the sender uses the strings"true"and"false", convert them with a comparison such as$d.disk_encrypted = "true". Don't use$boolean()for this:$boolean("false")returnstrue, because any non-empty string counts as true.
Send events
Send a POST request with a JSON body and the header Content-Type: application/json:
Add the header for your webhook's auth type. See Authentication.
Send many records
Send them as a JSON array (or a list inside an object) in a single request, up to the size limit. One request with many records is processed as one plan run. Avoid sending many requests for the same plan in quick succession. See Known limitations.
Responses
202
{"status": "accepted", "eventPk": "..."}
Event received and handed on for processing.
200
{"status": "duplicate"}
Sprinto already received this event. The plan does not run.
401
{"error": "Authentication failed"}
The key, token or signature is missing or wrong.
404
{"error": "Webhook not found"}
The URL is wrong, the webhook is inactive, or its ID was rotated.
413
Varies
The body is over the size limit. Bodies over 500 KB are rejected before the webhook reads them, and the response body may be an HTML error page rather than JSON. Check the status code, not the body.
429
{"error": "Rate limit exceeded"}
Too many requests in the last minute. Retry later.
500
{"error": "Internal server error"}
Something went wrong on Sprinto's side. Retry later.
Authentication
Choose the Auth Type on the webhook and describe it in Auth Config (JSON). Keys, tokens and secrets are stored as Sprinto credentials, and the Auth Config refers to them by credential ID.
API Key
Sprinto checks a header you name for a key stored in Sprinto.
The sender includes the key in that header:
Bearer Token
The sender includes the token in the Authorization header:
HMAC SHA256
The sender signs each request with a shared secret and Sprinto verifies the signature. Most SaaS tools that sign their webhooks (GitHub, Jira, Slack, Stripe, Linear, HubSpot and others) use this method.
secretCredentialPk
Yes
Credential ID of the shared secret.
signatureHeaderName
Yes
Header that carries the signature, for example X-Hub-Signature-256.
signaturePrefix
No
none
Text before the signature that Sprinto strips, for example sha256=.
signatureEncoding
No
hex
hex or base64.
signedStringTemplate
No
${body}
What was signed. Can combine ${body}, ${timestamp}, ${method} and ${url}, for example v0:${timestamp}:${body}.
timestampHeaderName
No
none
Header that carries the request timestamp in Unix seconds. When set, requests without it are rejected.
maxTimestampAgeSecs
No
300
Rejects requests whose timestamp is more than this many seconds from the current time.
signatureParseMode
No
direct
direct if the header holds only the signature. key_value if it holds pairs such as t=1700000000,v1=abc123.
signatureKey
For key_value
Key of the signature in a key_value header, for example v1.
timestampKey
For key_value
Key of the timestamp in a key_value header, for example t.
GitHub example:
Stripe example:
Sign the exact bytes you send. Sprinto computes the signature over the raw request body. If your code signs a JSON object and then serializes it again before sending, key order or whitespace can change and the signature won't match.
No Auth
Sprinto accepts any request to the URL. Use it only for testing.
Duplicate events
Sprinto ignores events it has already received, so a sender that retries won't cause extra plan runs. How it decides depends on the Event ID Config.
Not set
Its body is byte-for-byte identical to an earlier event.
{"source": "header", "key": "X-Event-Id"}
The value of that header matches an earlier event, even if the body is different.
{"source": "body", "path": "event.id"}
The value at that path in the body matches an earlier event, even if the rest of the body is different. path uses dot notation.
If the configured header or body field is missing from a request, Sprinto falls back to comparing the whole body.
Point the Event ID at a value that is unique for each event. If it points at something that repeats, such as a region, type or status, every event after the first is treated as a duplicate. Good choices are the sender's delivery ID header (for example
X-GitHub-Delivery) or an event ID field in the body.
When you test by hand, change the value the Event ID reads each time. With no Event ID Config, change any part of the body.
Filter events
A Filter Expression is an optional JSONata expression that decides whether an event runs the plan. It is evaluated against the request body, so refer to fields directly, without params.webhookPayload:
If the expression returns false, the event is marked filtered and the plan does not run.
If the expression can't be evaluated (for example, because of a syntax error), the event is not filtered out and the plan runs.
Map values to plan parameters
A Runtime Params Mapping copies values from the body into named plan parameters. Each key is a parameter name. Each value is a dot-notation path into the body, or $ for the whole body.
In the plan these are available as params.region and params.source. params.webhookPayload is always set as well.
If the plan declares a required parameter that has no default and the mapping doesn't provide it, the event fails with Required parameter "<name>" is missing.
URL verification challenges
Some services check a webhook URL before they send real events, by sending a challenge and expecting it echoed back. For services that send the challenge in a POST body (for example, Slack's url_verification), set a Challenge Config:
When a request's typeField equals typeValue, Sprinto responds 200 with {"challenge": "<value>"} and does not record an event or run the plan.
Limits
Request body size
500 KB (512,000 bytes)
Applies to every webhook. Max Payload Bytes can set a lower limit for a webhook, but not a higher one.
Rate Limit Per Minute
Default 60
Can be set from 1 to 1,000 per webhook.
Plan runs per plan
One at a time
See Known limitations.
Processing attempts per event
3
See below.
If processing an event fails before its plan run starts, Sprinto tries again after 30 seconds, then after 60 seconds. After the third failed attempt the event is marked failed. Failures inside a plan run are not retried this way. Check the plan run for those.
Known limitations
One run per plan at a time. While a plan is running, other events for the same plan are not queued. The events still show completed, but no records are stored from them, and their plan runs stay on Waiting for live progress. To avoid this, send many records in one request instead of many requests, and leave enough time between requests for the previous run to finish.
"Completed" isn't linked to the run's outcome. An event shows completed once it is handed to the plan engine. To confirm records were stored, check the plan run linked from the event.
Improvements to both are on the roadmap.
Monitor events
Open a webhook to see Recent Events. Each event shows its status, event ID, when it was received and processed, and a preview of the payload.
Received
Recorded and waiting to be processed.
Processing
Being checked against the filter and prepared for a plan run.
Completed
Handed to the plan engine. Open the plan run to see whether records were stored.
Filtered
The filter expression returned false. No plan run.
Failed
The event couldn't be processed, for example because a required plan parameter was missing.
Duplicate requests are not listed as events. They only get the duplicate response.
Troubleshooting
Map step reports every required field missing
The extract step reads a path that doesn't exist and returns an empty list
Use [$$.params.webhookPayload], plus the path to your records if they are nested
Map step rejects one or two fields
A key doesn't match the entity field name, or a value has the wrong type
Compare your mapping with the entity type definition
Event completed but the plan run stays on Waiting for live progress
Another run of the same plan was in progress when the event arrived
Send the records again. Batch them into one request, or space requests out
Event completed, run finished, but no data
A step in the plan run failed
Open the plan run and check the failed step
Every request returns duplicate
The Event ID reads a value that doesn't change between events
Point the Event ID at a unique header or field
Event filtered unexpectedly
The filter expression returns false for this body
Test the expression against the payload shown in Recent Events
Event failed with Required parameter ... is missing
The plan needs a parameter the mapping doesn't provide
Add it to the Runtime Params Mapping, or give the parameter a default in the plan
413
The body is over 500 KB
Split the records across several requests
401 on every request
Wrong header name or format, or the body changed after it was signed
Compare your request with the webhook's Auth Config
404
Wrong URL, the webhook is inactive, or the ID was rotated
Copy the URL again from the webhook page
Last updated

