Guest Machines

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 and managed in the console by a workspace owner or admin, and managing them isn't part of the API reference. Changing a webhook, rotating its secret, reading its deliveries and sending a test all need an owner or admin; the member who created a webhook and is no longer one can still see and delete it. Automation account keys and delegated tokens can't reach webhook management at all: they call only the operations the reference lists for them, so it answers 403 however they're scoped. A user API key acts with its member's role, so only an owner's or admin's key passes that check, and those routes carry no compatibility promise. 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:

HeaderContents
X-GuestMachines-SignatureComma-separated scheme=value elements; today, sha256=…
X-GuestMachines-TimestampUnix seconds, and part of the signed message
X-GuestMachines-EventThe event name, such as run.completed
X-GuestMachines-Delivery-IdStable 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. A delivery that fails all five attempts is listed for 7 days in the Failed deliveries section of Outbound Webhooks, where a workspace owner or admin can retry it; older failures stay in that webhook's delivery history.

Pausing a webhook (its Sending switch in Outbound Webhooks), or Guest Machines turning it off after repeated failures, holds its queued deliveries, including pending retries, without spending any of their five attempts. A delivery already on its way when you pause can still arrive. Held deliveries go out within ten minutes after you resume the webhook, with the attempts they had left. The console's test ping is still sent while a webhook is paused.

A webhook also sends nothing while the person who created it isn't a workspace owner or admin. If they're made a member or a viewer, its events wait instead of going out, and go out once they're an owner or admin again. Outbound Webhooks shows such a webhook as paused: an admin can delete it, or set it up again so it sends as them.

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 False

Sign 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 serialized body is capped at 64 KiB, including JSON escaping. Run events retain their run identity, status, and trigger correlation when present, even when the rest of the event is too large. Artifact descriptions are removed before artifact collections are shortened.

A reduced event sets data.output_truncated to true and includes data.truncation_reason. The optional data.artifacts_omitted and data.input_artifacts_omitted fields give the number of entries left out of each collection, including entries beyond its 100-item projection limit. data.artifact_descriptions_omitted counts descriptions removed before the collections are shortened, so it may include entries also counted as omitted. data.omitted_fields names any other event fields removed to fit the limit. Treat these as a partial notification; retrieve the identified run and its files 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

EventMeaning
run.startedExecution began
run.completedFinished successfully
run.failedFinished unsuccessfully
run.cancelledStopped by a person or by policy
run.pausedStopped awaiting intervention — not finished
run.resumedContinued after a pause
run.requires_approvalWaiting for a tool call, or a run's tools, to be approved

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 plan (the run's tools need approving; "Approve tools for this run" in the app), retrieve the active plan, then approve or deny the tools, or change them with replan. 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

Workflow runs, run governance, alerts, and budgets all publish events too. A workflow run sends workflow.run.* events rather than run.*, and a filter on workflow_ids limits them to chosen workflows. budget.breach.warn and budget.breach.critical carry the budget's scope, plus its workflow_id, agent_id, or service_principal_id when the budget covers a workflow, an agent, or an automation account. Webhook events is the full catalog, grouped by what each event reports on.

Conversations between agents send thread.* events to every webhook in the workspace that subscribes to them; agent filters don't apply. Each carries thread_id; the request events also carry thread_request_id, requester_agent_id and responder_agent_id, and thread.reply_received its outcome (answered or declined). thread.closed carries the reason. A conversation with a private agent sends no events. The events never include message text; see let agents talk to each other.

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:

FieldContents
pause_reasonStable machine-readable code, e.g. daily.cost_cents
pause_categoryBroader class, e.g. resource_limit, checkpoint_failure
resumableWhether resuming can carry the run to a terminal event
reasonHuman-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.

A 202 acknowledges an accepted event waiting to start; it does not report a completed run. Each webhook has a separate limit on these waiting events (100 by default). When that queue or the per-minute rate window is full, new events receive 429 with Retry-After: 60 and have not been accepted. Retry them later with the same Idempotency-Key and payload. A retry of an already accepted event returns its original acknowledgment even when capacity is full; reusing the key with different data returns 409. Queue pressure never evicts an accepted event. The Inbound Webhooks page shows Waiting to start and Queue full, refreshing every 15 seconds while visible. This count excludes executing runs and runs paused for approval.

Rotating the URL or pausing the webhook also rejects requests still uploading their bodies.

Select Run One at a Time when events must be handled in order. The webhook then starts one run at a time, oldest event first; a later event waits until the current run finishes or is cancelled, including while that run waits for an approval. Waiting events count toward the limit above.

A member can also create a private webhook, which runs one of their own agents with their own app accounts, such as Gmail, Outlook or GitHub. To your sender its trigger URL works the same way, with these differences:

  • It always runs one at a time.
  • It accepts events only after its owner reviews it and turns it on, and answers 410 while it is off. Events it already accepted stay queued and start when it is turned back on.
  • The payload is task data only. It cannot change the Google account, add permissions, skip the owner's approvals, or send results anywhere else.

Only its owner can see, edit, or rotate a private webhook. Organization admins can stop or delete it but cannot read its configuration. See use apps.

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 cost center 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 workflow (always one agent for a private webhook), fixed when the webhook is created. Your request, the trigger URL, and the 202 are identical either way; what differs is duration. A workflow run has several steps 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 an agent or workflow that couldn't run unattended is refused at creation, and the refusal names what would stop it — see build a workflow.

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.

On this page