Marble 2 beta
Webhooks
Instead of polling an operation, ask Marble to call your server when it
finishes. Put a webhookUrl on any task request and Marble POSTs one signed
event to it when the operation reaches SUCCEEDED or FAILED, retrying for
up to 24 hours until your server acknowledges it.
Webhooks are a Developer API feature: they apply to tasks submitted with an API key.
Request a callback
Add webhookUrl to the task body. The URL must be https on port 443, name
a public DNS host (not an IP address), and carry no credentials or fragment.
Anything you need to correlate the callback with your own records can ride
the query string; it is echoed back exactly and is not covered by the
signature.
The response is the same Operation a request without a callback returns.
The accepted destination is read back on the operation's webhookDelivery
record, described below. A request selects one outcome channel:
webhookUrl cannot be combined with ?wait=true.
Receive the event
Your server receives one POST per completion with a JSON body:
| Field | Meaning |
|---|---|
type | task.succeeded or task.failed. Cancelled operations send no event. An operation that expires also sends task.failed, with data.state FAILED. |
createdAt | When the operation finished, not when this attempt was sent. |
data | The operation's IDs and task name. task.failed adds data.error.code, the operation's error code. |
The event is deliberately thin. It carries no inputs, results, or asset URLs.
Fetch the result with GET /api/v2/operations/{operationId} using a project
API key, exactly as you would after polling; receiving an event grants no
access on its own.
Answer with any 2xx within 15 seconds. Do the work after acknowledging: a
slow handler is retried as if it had failed.
Verify the signature
Every request carries three headers following the Standard Webhooks specification:
| Header | Value |
|---|---|
webhook-id | The delivery ID (del_...), the same on every retry of one event. Use it to deduplicate. |
webhook-timestamp | Unix seconds when this attempt was sent. Reject attempts older than a few minutes, or a captured request stays replayable. |
webhook-signature | One or more space-separated v1a,<base64> values: Ed25519 signatures over {webhook-id}.{webhook-timestamp}.{raw body}. |
Marble signs every callback with its own key and publishes the public keys as
a JWKS at https://api.atlas-beta.worldlabs.ai/.well-known/webhooks/jwks.json (the host
of your API base URL; keys are kty: OKP, crv: Ed25519). There is no
secret to generate or store. Fetch the document, cache it for up to 24
hours, and accept the request if any signature verifies against any listed
key: the header does not say which key signed. When none verifies, refetch
the JWKS once before rejecting, since a key may have rotated. Any Standard
Webhooks library that supports the asymmetric v1a scheme verifies these
headers as-is.
When Marble rotates its key, every callback carries a signature from both the new and the old key and the JWKS lists both for at least 24 hours, so a receiver holding either key keeps verifying.
One key signs every account's callbacks, so a valid signature proves the
event came from Marble, not that it was meant for your receiver. If staging
and production receivers must not act on each other's callbacks, have each
receiver check data.projectId against the project it serves and drop
anything else. An event carries no results, so a callback that reaches the
wrong receiver can only prompt a read of an operation that receiver's API key
cannot see.
Retries and states
A 2xx marks the delivery DELIVERED. A 410 Gone marks it SUPPRESSED
with no further attempts. A webhookUrl whose host resolves to a private or
internal address is marked EXHAUSTED after its first attempt, since Marble
never sends to such an address. A first send that fails with a 5xx, a
timeout, a connection or TLS error, or a host that does not resolve yet gets
one fast retry about 2 seconds later, skipped when the response carries a
valid Retry-After header. Those failures, and any other answer that is not
a 2xx or 410 (including a 4xx and a redirect: Marble does not follow
redirects), are retried with growing delays (30 seconds doubling to 4 hours,
±10% jitter) until 24 hours after the operation finished, then marked
EXHAUSTED. A Retry-After header on any retried response is honored when
it asks for a later time than the schedule.
Read the delivery record for any operation that asked for a callback:
| State | Meaning |
|---|---|
SCHEDULED | The operation is not finished yet, or its completion is on its way to the webhook service. |
PUBLISH_FAILED | Marble could not hand the completion to its webhook service. Poll the operation instead. |
PENDING | Waiting for the next attempt; nextAttemptTime says when. |
IN_FLIGHT | An attempt is running. |
DELIVERED | Your receiver acknowledged the event. |
SUPPRESSED | Your receiver answered 410. |
EXHAUSTED | The 24-hour window closed without an acknowledgement, the webhookUrl host resolves to a private address and no attempt can succeed, or the operation's content was purged. |
The record also carries attemptCount, event (the exact body sent), and
lastAttempt, whose httpStatus or errorCategory describes the most
recent send or, if nothing was ever sent, why the delivery closed. Records
are kept for 90 days. Purging an operation's content ends a delivery still
waiting to be sent (EXHAUSTED, no further attempts), and the record stays
without the destination. An operation that never asked for a callback
answers 404, and so does a cancelled one: a cancel sends no event.
To check many operations at once, list them: operations that asked for a
callback may carry a webhookDelivery summary with state, attemptCount,
and lastAttemptTime. It is absent after cancellation or when the webhook
service cannot be read.
Test your receiver
There is no endpoint to send a test event. Submit a real task with
webhookUrl set to your receiver and use that delivery to confirm your
handler and signature verification work.