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

Webhooks

Receive a signed HTTP callback when a map, map set, or tracked object changes state, instead of polling the API for status.

A map takes minutes to process, and an object or map set takes as long as its inputs need. Until now the only way to find out when one finished was to poll Map Details on a timer.

Webhooks invert that. Register one HTTPS endpoint, subscribe to the events you care about, and MultiSet POSTs a signed JSON payload to your server the moment a resource changes state.

Webhooks are configured in the Developer Portal under Credentials -> Webhooks, or over the REST API. For the endpoint reference see Webhooks API.

Quick start

  1. Build a receiver. A single route that accepts POST, returns 200 quickly, and can echo a value back from the request body. See Verifying your endpoint.

  2. Register it. Portal, or POST /v1/webhook/endpoint with a url and the events you want.

  3. Save the signing secret. It is returned once at registration and never again.

  4. Verify the signature on every request before you act on it. See Verifying the signature.

One endpoint per account. To change where events go, update or delete the existing endpoint.

Verifying your endpoint

Registration is not complete until MultiSet has proved your URL reaches real code. As soon as you register, a signed endpoint.verify event is POSTed to it:

{
  "id": "evt_verify_9f2b1c9d4e7a6b3f",
  "type": "endpoint.verify",
  "apiVersion": "2026-09-01",
  "occurredAt": "2026-09-08T09:14:22.184Z",
  "livemode": false,
  "accountId": "66eb01b11d9025665dbbba79",
  "data": { "challenge": "ch_4f8a2b7c1d9e3f6a8b2c5d7e9f1a3b4c" }
}

Your endpoint must reply 200 with a JSON body that echoes the challenge back:

The challenge has a 10 second timeout. On success the endpoint becomes active and events start flowing. On failure it stays pending_verification, the response tells you why, and you can fix your server and re-run verification without re-registering.

An endpoint only receives events while its status is active. Events raised while it is pending_verification or disabled are dropped, not queued.

Events

Twelve event types, across three entities:

Entity

created

pending

active

failed

map

Map record created, upload not finished

Upload landed, processing started

Processing finished, map is queryable

Processing failed

mapset

Map set created

Overlap computation started

At least one overlap succeeded

The only target failed

object

Object record created

Upload landed, tracking started

Tracking finished, object is queryable

Tracking failed

Subscribe with exact names, an entity wildcard, a state wildcard, or everything:

events entry

Matches

map.active

That one type

map.*

Every map event

*.failed

Every failure, on all three entities

*

Everything, including event types added later

A typo in a subscription is rejected at registration, so map.faild fails loudly instead of silently never firing.

The data block

data is shaped per entity. Every event carries the resource's codes, its new status, and the status it came from:

A failed event adds an error object:

type is USER_ERROR when the input was the problem and re-uploading a good file will fix it, or SYSTEM_ERROR when it was ours. message is truncated to 500 characters.

The delivered request

POST with Content-Type: application/json and these headers:

Header
Value

Multiset-Event-Id

evt_1a0636bc890d38c1f72d2b9d122, unique per event. Dedupe on this.

Multiset-Event-Type

map.active

Multiset-Signature

t=1788876000,v1=5257a869e7...

User-Agent

Multiset-Webhooks/1.0

The body wraps your data block in a fixed envelope:

livemode is false for verification and test events, true for real ones.

Verifying the signature

Anyone who learns your URL can POST to it. The signature is what tells you a request came from MultiSet, so verify it before you act on the body.

Multiset-Signature holds a timestamp and an HMAC:

v1 is HMAC_SHA256(secret, "{t}.{rawBody}"), hex encoded. This is the same scheme Stripe uses, so if you already verify Stripe webhooks the code carries over.

The timestamp is there so you can reject a replay of an old but validly signed request. A tolerance of 300 seconds matches what MultiSet uses.

Responding

Return any 2xx to acknowledge. Anything else, or no response within 10 seconds, counts as a failure and is retried.

Acknowledge first, process afterwards. Do the slow work in a background job rather than inside the request. A handler that spends 15 seconds building a mesh preview will time out, be retried, and build it again.

Redirects are not followed. A 301 or 302 is recorded as redirect_refused, because the destination has not been through the address checks the original URL passed. Register the final URL.

Retries

A failed delivery is attempted five times with a widening gap:

Attempt
Sent

1

immediately

2

1 minute after attempt 1

3

5 minutes after attempt 2

4

30 minutes after attempt 3

5

2 hours after attempt 4

After the fifth attempt the delivery is marked exhausted and never tried again. The whole chain spans about 2 hours 36 minutes, so a receiver that comes back inside that window still gets its events, and one that is down longer does not.

An exhausted delivery is not recoverable through the API: there is no replay endpoint. If your receiver was down for longer than the retry window, reconcile by listing the resources you care about, for example with Map details, rather than waiting for the events to come back.

Sustained failure disables the endpoint. After 20 consecutive failed deliveries the endpoint is set to disabled and delivery stops. Fix your server, then re-run verification to re-enable it, which also resets the failure count. Consecutive means across events, not attempts within one chain, so a single flaky hour will not disable you.

Idempotency and ordering

Delivery is at-least-once. A timeout on your side after you already processed the event still counts as a failure, so the same event can arrive twice. Keep a record of Multiset-Event-Id values you have handled and ignore repeats.

Events can arrive out of order. Use occurredAt, set by the service that raised the event, rather than the order requests reach you. A map.active arriving before the map.pending for the same map is possible under retry.

Test events

Send a sample event to your endpoint on demand from the portal, or with POST /v1/webhook/endpoint/{id}/test. It is signed exactly like a real event, carries livemode: false, is never retried, and reports back what your server answered, including the response code, the timing, and the first 2KB of your response body.

This is the fastest way to debug a receiver: it turns a silent integration into a request you can watch.

Delivery log

Every attempt is recorded. Query it from the portal or with GET /v1/webhook/delivery, filtered by event type, status, date range, or resourceId to see everything that happened to one map. Fetch a single delivery by its event id to read the exact payload that was sent, plus one entry per attempt with the response code and timing.

Use it to answer "did you send it?" without guessing.

Endpoint requirements

  • HTTPS in production.

  • Publicly resolvable. Private, loopback, and link-local addresses are rejected, and the address is re-checked at delivery time, not just at registration, so an endpoint whose DNS is later repointed inward stops being delivered to.

  • Responds within 10 seconds.

  • No redirects.

  • Returns JSON containing challenge for the endpoint.verify event.

Rotating the secret

POST /v1/webhook/endpoint/{id}/rotate-secret returns a new secret once. The previous secret stops verifying immediately, so there is no overlap window: deploy code that reads the new secret from configuration, then rotate, or accept a brief period of signature failures. Those deliveries are retried, so nothing is lost if you rotate and redeploy inside the retry window.

Rotate if the secret may have been exposed, for example in a log, a screenshot, or a committed config file.

Last updated

Was this helpful?