For the complete documentation index, see llms.txt. This page is also available as Markdown.

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:

  1. Size. A body larger than the size limit is rejected with 413. See Limits.

  2. Webhook lookup. The webhook must exist and be active. If not, Sprinto returns 404.

  3. Rate limit. Requests over Rate Limit Per Minute are rejected with 429.

  4. Authentication. A request that fails the webhook's auth check is rejected with 401.

  5. Duplicate check. An event Sprinto has already received returns 200 with {"status": "duplicate"} and does not run the plan.

  6. Event recorded. Sprinto saves the event and returns 202 with {"status": "accepted", "eventPk": "..."}.

After the 202, Sprinto processes the event in the background:

  1. Filter. If the webhook has a filter expression and it evaluates to false, the event is marked filtered and the plan does not run.

  2. Plan run. Sprinto starts a run of the attached plan with the event's body.

A 202 confirms 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, event or body. 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:

  1. A transform step that reads params.webhookPayload and returns a list of records.

  2. A mapEntityType step that turns each record into the fields of your entity type.

  3. An insertCustomEntities step that stores the records.

  4. An evaluateEntities step 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

What the sender posts
Extract expression

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 mapEntityType step 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, identifier and every other field the entity type marks as Required.

  • Don't map lastSeenOnSource. Sprinto sets it when the record is stored.

  • identifier must be unique and stable for each record. A later record with the same identifier updates 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 true or false. 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") returns true, 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

Status
Body
Meaning

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.

Field
Required
Default
What it does

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.

Event ID Config
An event counts as a duplicate when

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

Limit
Value
Notes

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.

Status
Meaning

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

What you see
Likely cause
What to do

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