Docs

Webhooks

Be told when a sandbox is made, starts, stops, fails or is deleted, and when mail arrives for one.

For your agent

agental.io/docs/webhooks.md
POST/v1/webhooks
curl -X POST https://agental.io/v1/webhooks \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/agental","events":["sandbox.created","sandbox.error"]}'
  • The answer includes signingSecret, once. Keep it.
  • events names what to send, from the table below, or * for all of them. Leave it out to get all of them.
  • url starts with http:// or https:// and is on the public internet. Redirects are not followed.

Events

EventSent when
sandbox.createdA new sandbox runs for the first time.
sandbox.startedA sandbox runs again after it was stopped: a start you asked for, one from storage, or the start that follows a move to another server. resumed is true when its stop kept its memory and the start put it back.
sandbox.stoppedIt stops. reason says why: asked, ttl when its time limit stopped it, billing when the plan did, or move when it was stopped to be moved to another server and is started again there. memoryKept is true when its memory was kept.
sandbox.archivedIts disk was moved to storage. It is still stopped.
sandbox.movedIts machine was moved to another server. restarted says whether what ran in it was lost.
sandbox.errorIt went to error. error says why.
sandbox.deletedIt was deleted. operationId is the deletion. reason is schedule when a schedule that runs once deleted it after its run.
sandbox.auto_stop_setIts stop was moved. autoStopAt is when it stops now, previousAutoStopAt when it would have, and by is account or sandbox, who moved it.
sandbox.labels_changedIts own tags or metadata changed, or it was put on another template. tags and metadata are what it shows now, previousTags and previousMetadata what it showed before. by is sandbox, or template with templateId for a move to another template.
template.createdA template was made (an account's default is made without one). It carries its start, tags and metadata. Every template.* event has templateId, name, and default, true for the account's default template, and is sent once, to the team's webhooks: the template's sandboxes are sent nothing.
template.updatedA template's name, start (size, snapshot, time limit), network rule or SSH (turned on or off, a key added or removed) changed. changed lists which, of name, start, network and ssh, and each one listed is there with what it is now. A change to the same thing again sends nothing. What a sandbox already made has is not changed: it takes a network rule or secrets when it next starts or is applied to.
template.secrets_changedOne of a template's variables or secret files was set, had its hosts changed, or was removed. change is set, hosts_changed or removed; variable has its name, and when it is set, hidden and hosts, or file is its path. variables, hidden and files name what it has now. No value is ever in it. A sandbox's own secrets send nothing.
template.labels_changedA template's own tags or metadata changed: tagsAdded, tagsRemoved, metadataSet and metadataRemoved say how, and tags and metadata are what it has now. default is true for the account's default template, whose sandboxes include the ones made naming none (their templateId is null). It is sent once, to the team's webhooks, and its sandboxes are sent nothing: they show the template's from then on. To keep your own record of them, read the sandboxes on it again, or merge the change in.
template.deletedA template was deleted. name is the name it had.
prompt.createdA prompt was given to it.

What is sent

json
{
  "id": "evt_9f2c41d7a05b6e38",
  "type": "sandbox.stopped",
  "createdAt": "2026-10-09T15:45:00.412Z",
  "data": {
    "sandboxId": "sbx_2a9a47e50daec64a",
    "name": "build-agent",
    "status": "stopped",
    "previousStatus": "running",
    "tags": ["ci", "pr-412"],
    "metadata": { "repo": "acme/web" },
    "stopId": "stp_0b7d2e9a41c6f853",
    "reason": "ttl"
  }
}
  • Every sandbox. event has the sandbox's sandboxId and name, its status now and its previousStatus, and its tags and metadata as they were then, its template's with its own.
  • id is the same in every webhook the event is sent to, and in the log record your collector is sent. Use it to tell one event from another.
  • An event is written with the change it is about, and is sent once that change is made. A stop answers after its webhooks were sent.
  • Each webhook is sent each event until it answers with a 2xx, waiting 2 seconds for each answer: see below. GET /v1/sandboxes/{id}/events lists every event a sandbox had.

Email events

EventSent whenWhat `data` carries
email.receivedA message for a sandbox was taken and kept.messageId, sandboxId, to, from, subject, sizeBytes, attachments, checks, spamScore, sentByProgram, and url, where the message is read.
email.deliveredThe recipient's server took a message a sandbox sent.messageId, sandboxId, to.
email.bouncedA message a sandbox sent was refused, or could not be delivered within a day.messageId, sandboxId, to, reason, which is refused or expired, and detail, which is what a server said.

They are sent whether the sandbox is running or not. See Email.

Check the signature

Each request carries X-Agental-Signature: sha256=<hex>: the HMAC-SHA256 of the raw body, keyed with your signing secret. Work it out yourself and compare before you trust the body. POST /v1/webhooks/{id}/rotate gives a new secret, once.

The SDKs do the comparison. Pass the body exactly as it arrived, before any JSON parsing:

import { verifyWebhook } from "agental-sdk";

const genuine = await verifyWebhook(rawBody, request.headers.get("x-agental-signature"), signingSecret);

When your server does not answer

  • An event counts as taken when your server answers 2xx. Anything else, or no answer in time, and it is tried again.
  • An event is tried eight times over about a day: at once, then after 1, 5 and 15 minutes, and after 1, 3, 6 and 12 hours.
  • Every try carries the same id, so a receiver that has taken an event can ignore it when it comes again.
  • GET /v1/webhooks/{id} lists the webhook's recent deliveries, with the status your server answered. Each one is a try, and says which event it was a try of as eventId, and which try as attempt. Tries are kept for 30 days.

In the console

  • The Webhooks tab lists your webhooks beside the kinds of event, wired to the ones each is sent. Point at a webhook to light what it takes, or at a kind to light the webhooks that take it. A failing webhook's wires are dashed.
  • Add a webhook ticks the kinds on the same drawing. The signing secret is shown once, on the webhook's page that opens next, with how to check it.
  • A webhook's Deliveries draws its last 24 hours, a line for each kind, and each event it did not take at the first try on the schedule of eight tries, with the answer to every try and when the next is due.
  • Its Settings change the address, the description and the kinds, make a new secret and delete it, and each of the last two asks first.
  • A sandbox's Email tab says so when none of your webhooks takes email.received.

From the CLI

bash
agental webhooks
agental webhook add https://example.com/hooks/agental --events sandbox.stopped,sandbox.error --description "build bot"
agental webhook show <webhook-id>
agental webhook update <webhook-id> --all
agental webhook rotate <webhook-id>
agental webhook delete <webhook-id>

add prints the signing secret, once. --all is every event, those added later included. show lists each event the webhook did not take at the first try, with the answer to every try.

The full machine-readable reference is OpenAPI 3.1. Every page as Markdown, for an agent: llms.txt. Questions: [email protected]