Webhooks
Receive execution events and start governed work from external systems.
Guest Machines supports two different webhook directions. Outbound webhooks notify your system about platform events. Inbound webhooks let an external system start a configured flow.
Outbound webhooks
Create a webhook for the events your integration actually consumes. Your endpoint should return a successful response quickly and move expensive processing to a queue.
Webhooks are created in the console by an organization admin or owner, which is why they are absent from the API reference: an API key has no role, so it cannot create one however it is scoped. The delivery contract below is what your integration builds against.
Design the receiver to tolerate duplicate delivery, delayed delivery, and events arriving close together.
Delivery headers
Every delivery is signed. Guest Machines sends a JSON body with these headers:
| Header | Contents |
|---|---|
X-GuestMachines-Signature | Comma-separated scheme=value elements; today, sha256=… |
X-GuestMachines-Timestamp | Unix seconds, and part of the signed message |
X-GuestMachines-Event | The event name, such as run.completed |
X-GuestMachines-Delivery-Id | Stable across retries; also present in the body |
Use X-GuestMachines-Delivery-Id as the deduplication key.
Delivery semantics
Delivery is at least once. Any 2xx response marks the delivery successful;
the response body is ignored. A network failure, timeout, redirect, or non-2xx
response is retried up to five total attempts with exponential backoff starting
at 30 seconds. The delivery ID stays the same across those attempts, while the
signature timestamp is generated again for each attempt.
Events can be delayed, duplicated, or delivered out of order relative to a different event. Persist the delivery ID before starting side effects, make the handler idempotent, and respond quickly. Do not hold the request open while performing agent work or another expensive operation.
Verify the signature
The signing secret is shown once when you create the webhook, and again when you rotate it. Store it in a secret manager.
The signed message is the timestamp, a literal ., then the exact raw request body. Compute HMAC-SHA256(secret, "{timestamp}.{body}") and hex-encode it.
Parse the signature header rather than comparing it whole. It is a comma-separated list of scheme=value elements: find the one whose scheme is sha256, compare its value in constant time, and ignore any element whose scheme you do not recognize. A receiver written this way keeps working when a new algorithm is added alongside the current one.
import hashlib
import hmac
def is_signed_by_guest_machines(secret, body_bytes, timestamp, signature_header):
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + body_bytes,
hashlib.sha256,
).hexdigest()
for element in signature_header.split(","):
scheme, _, value = element.strip().partition("=")
if scheme == "sha256" and hmac.compare_digest(value, expected):
return True
return FalseSign against the raw bytes you received. Re-serializing the parsed JSON changes key order and whitespace, and the signature will not match. Reject deliveries whose timestamp is far outside your clock tolerance, and reject unsigned requests outright rather than falling back to trusting them.
Rotating a secret invalidates the old one immediately, so update the receiver before rotating.
Event payload
The body is a JSON object with contract_version, the event name, a timestamp,
the delivery id, and a data object carrying the event's public fields. An
optional request_id correlates events emitted inside an API request with Guest
Machines logs. It is omitted for background-originated events.
Validate the fields you need and ignore the ones you do not recognize — new fields and event types are added over time, and a receiver that rejects unknown values will start failing on a release it did not ask for. The complete machine-readable envelope and event catalog are in the outbound webhook contract.
{
"contract_version": 1,
"event": "run.completed",
"timestamp": "2026-08-10T02:43:10.969970+00:00",
"delivery_id": 17,
"data": {
"run_id": 49,
"status": "completed",
"trigger": {
"delivery_id": "your-own-key"
},
"verification_status": "verified"
}
}The top-level delivery_id identifies this delivery. Use it to deduplicate, not to identify your request.
The body is capped at 64 KiB. Webhooks summarize an event; retrieve the identified run for the complete public result.
Correlating an event with the request that caused it
When an inbound webhook starts the run, include your own string or integer
delivery_id in that trigger body. Run events return it as
data.trigger.delivery_id, so the notification identifies your request without
echoing the rest of the arbitrary inbound payload. Runs started another way, or
trigger bodies without that field, have no trigger.
Run lifecycle events
| Event | Meaning |
|---|---|
run.started | Execution began |
run.completed | Finished successfully |
run.failed | Finished unsuccessfully |
run.cancelled | Stopped by a person or by policy |
run.paused | Stopped awaiting intervention — not finished |
run.resumed | Continued after a pause |
run.requires_approval | Waiting on a plan or tool approval |
Subscribe to the terminal events and to run.paused and run.resumed. A run that pauses emits nothing further until someone acts on it, so a consumer subscribed only to run.completed and run.failed waits indefinitely on a run that has already stopped.
run.requires_approval carries approval_kind as plan or tool. For a plan
approval, retrieve the active plan
and use the public approve, deny, or replan controls. For a tool approval,
retrieve the pending interaction
and submit the attributed decision through the
interaction response.
A generic resume call cannot supply or bypass that decision.
Other event families
Pipelines, workforce runs, run governance, alerts, and budgets all publish events too. Webhook events is the full catalog, grouped by what each event reports on.
The console's test action sends ping, which is a delivery test rather than a
subscribable platform event. Capability discovery returns both the current
subscribable catalog and that test-event name.
Pausing is not an outcome
run.paused means the run stopped and is waiting. It carries:
| Field | Contents |
|---|---|
pause_reason | Stable machine-readable code, e.g. daily.tokens |
pause_category | Broader class, e.g. resource_limit, checkpoint_failure |
resumable | Whether resuming can carry the run to a terminal event |
reason | Human-readable sentence, for display only |
Branch on pause_reason, not on pause_status: the reason code is the contract, while the status string is a lifecycle detail that gains members. A quota pause and a platform fault both arrive as run.paused and need opposite handling — the first clears when the limit is raised, the second needs investigation.
When resumable is true, the run can continue once its blocker is cleared,
emitting run.resumed and then a terminal event. Do not treat run.paused as
a final answer: publishing a conclusion there and closing the work leaves a
stale result when the run later completes.
Stability
The envelope, required headers, signing message, current sha256 scheme, and
delivery ID semantics are the public delivery contract. Future algorithms will
be added as further elements in X-GuestMachines-Signature rather than
replacing sha256, so a receiver that selects the scheme it knows keeps
verifying without changes. New event types and event-data fields are additive;
ignore what you do not recognize.
This commitment covers webhook delivery only. HTTP request compatibility is governed separately by the API reference.
Inbound webhooks
An inbound webhook exposes a trigger URL for a specific automation. Treat the trigger token as a secret. Rotate it if it is disclosed and update the sending system immediately.
Validate and constrain payloads before they reach expensive agent execution. A public trigger should not become an unbounded prompt relay.
Enable Require the run to create a file when every triggered run must return a downloadable file. The API stores this setting as require_artifact; a matching run must create at least one file and pass the artifact_requirement check. If the target also has an output schema, both checks must pass before the run is verified. This setting belongs to the authenticated webhook configuration. A sender payload field named require_artifact remains ordinary data under input_data.webhook.payload and cannot change it.
An inbound webhook can also provide fixed input files through its authenticated
artifact_inputs configuration. These files are chosen by the webhook owner,
authorized when the configuration is saved, and authorized again whenever the
webhook fires. The public sender cannot add, remove, or replace them through its
payload. Schedules use the same fixed-file behavior.
In a shared organization, a webhook can also carry Project, Ticket, and Cost center tags, stored as project_tag, ticket_tag, and cost_center_tag. Every triggered run is stamped with them, which is the only way webhook-driven work reaches the chargeback breakdown — nobody is present at trigger time to be asked. These belong to the webhook configuration for the same reason require_artifact does, and it matters more here: a trigger URL is public, so a sender that could name a cost center could file your organization's spend against any team it chose. Identically named fields in a payload stay ordinary data under input_data.webhook.payload.
The automation behind the URL is exactly one agent or one team, fixed when the webhook is created. Your request, the trigger URL, and the 202 are identical either way; what differs is duration. A team run fans out and routinely takes longer than a single agent's, and often longer than the webhook waits to record an outcome, so read the result from the runs endpoints or an outbound webhook rather than from the webhook's last recorded outcome. Pointing a webhook at a team is also refused at creation unless every member can run unattended — see build a team.
Operational checklist
- Use HTTPS.
- Verify the signature on every delivery, and reject anything unsigned.
- Store received delivery IDs.
- Respond before doing expensive work.
- Retry only transient failures.
- Monitor repeated delivery failures.
- Never log credentials or full sensitive payloads.