Skip to main content

Delivery and batching

Request format

Each delivery is an HTTPS POST to your endpoint with Content-Type: application/json; charset=utf-8, plus any custom headers and the X-XY-Signature header you have configured. See Securing your endpoint.

Each attempt has a 10 second timeout. Anything other than a 2xx response — including a timeout or a connection failure — is treated as a failed delivery and retried.

The batch envelope

The request body is a JSON object wrapping the events, not a bare array. Field names are PascalCase.

{
"Id": "3f1b8f0e-2c44-4a1d-9f52-6b3f0a2d1c77",
"GroupId": "0b26f8f3-5d1c-4c6a-9d3e-1a2b3c4d5e6f",
"Type": "Occupancy",
"CustomerId": "2b7f9e10-4c3a-4b8e-9a11-7d2e5f6c8b90",
"Created": "2026-06-02T04:17:40Z",
"Sent": "2026-06-02T04:17:41Z",
"Data": [
{
"FloorSpaceId": "ac52e43d-7c21-4464-ab21-d0ace07a94b1",
"OccupancyStatus": "CurrentlyOccupied",
"Headcount": 2,
"CollectedDate": "2026-06-02T04:17:39.1066221Z",
"OccupancyStatusChangeDate": "2026-06-02T04:17:39.1066221Z",
"PreviousOccupancyStatus": "RecentlyOccupied"
}
]
}
FieldTypeNotes
Idstring (GUID)Batch identifier. See Idempotency for when it is stable across retries.
GroupIdstringThe location the batch was grouped by. Only present when GroupByLocation is enabled; the field is omitted otherwise.
TypestringOccupancy, FloorSightings, or SpaceSensorReading. Note these differ from the GraphQL webhookType enum values.
CustomerIdstring (GUID)Your organisation. Useful when several XY Sense organisations post to one receiver.
Createdstring (ISO8601)UTC time the batch started filling.
Sentstring (ISO8601)UTC time this attempt was sent. Changes on every retry. Compare with Created to measure delivery delay.
DataarrayThe events. See Event payloads.

Null fields are omitted rather than sent as null, so treat any field documented as optional as absent rather than null.

Ordering and acknowledgement

  • Events are queued per webhook and delivered in order.
  • A 2xx response acknowledges the whole batch. There is no partial acknowledgement.
  • Because ordering is preserved, a batch your receiver keeps rejecting blocks everything behind it. Return 2xx once you have durably accepted a batch and handle parse or business errors out of band, rather than rejecting the delivery.

Batching

  • A batch is sent when either maxBatchSize (100–1000 events) or maxBatchDelayMs (1 second–5 minutes) is reached.
  • A single batch can contain multiple events for the same entity — for example two headcount changes for one space within the same window. Process Data in order rather than keeping the first or last match per entity.

Idempotency

Delivery is at-least-once. A batch that times out or fails after your receiver has already committed it will be sent again, so processing must be idempotent.

Id identifies the batch, but its stability across retries depends on configuration:

  • Default (GroupByLocation off): Id is generated once per batch and is stable across all retry attempts. You can dedupe on it.
  • GroupByLocation on: a fresh Id is generated for each retry attempt, so it will not catch a redelivered batch.

For a scheme that works in both cases, dedupe on the event itself: the entity identifier plus its timestamp (FloorSpaceId + CollectedDate, FloorId + CollectedDate, or SpaceSensorId + CapabilityName + CollectedAt) is unique per event.

Retries and failure handling

  • Failed attempts are retried with exponential backoff (2 seconds, 4, 8, …), capped at 10 minutes between attempts.
  • Retries continue for up to 24 hours.
  • If an endpoint is still failing after 24 hours, the webhook is stopped and set to enabled: false. Its queued events are not delivered. Re-enable it with updateWebhook once the endpoint is healthy, then backfill the gap with the corresponding GraphQL history query, subject to its retention window.

Recovering a backlogged receiver

If your receiver has fallen a long way behind, use resetWebhookConsumer to skip to the latest queued event, then backfill the gap through GraphQL polling queries. See Resolve event data with GraphQL.

Compression

Set compressionType on the webhook to Gzip or Deflate to have request bodies compressed. Your receiver must decompress the body, and must sign against the decompressed bytes when validating signatures. Compression is off by default.

Configuration options

addWebhook and updateWebhook accept configOptions as key/value pairs:

KeyDefaultApplies toEffect
ExcludeHeadcountChangesfalseOccupancyChangesDrop events where only the headcount changed. Cuts volume by roughly 90%.
GroupByLocationfalseOccupancy, space sensorsSend one batch per location and populate GroupId. Note the Id caveat.
OnlyComfortReadingsfalseSpaceSensorReadingsDeliver only comfort readings.
EnableBatchingtrueFloorSightings onlySet to false to receive one event per request. Ignored for occupancy and space-sensor webhooks, which are always batched.

Example: "configOptions": [{ "key": "ExcludeHeadcountChanges", "value": "true" }]

Service limits

LimitValue
Batch size100 to 1000 events
Batch delay1 second to 5 minutes
Per-attempt timeout10 seconds
Retry backoff cap10 minutes
Total retry window24 hours, after which the webhook disables
Queue depth per webhook1,000,000 events

Managing webhooks

Everything available in the Admin Console is also available via GraphQL (requires WritePublicApi on the key):

  • webhooks / webhook(id:): list and inspect registered webhooks, including delivery metrics.
  • updateWebhook: change the endpoint, batching, headers, secret, compression, or config. Set enabled: false to pause delivery.
  • deleteWebhook: remove a webhook.
  • resetWebhookConsumer: skip to the latest queued event.
  • resetWebhookMetrics: clear delivery metrics.