Docs: Webhooks for agent and sandbox events | Maritime

Webhooks

A Maritime webhook is an HTTPS endpoint of yours that Maritime POSTs a signed JSON event to when a customer's agent changes state, so you do not have to poll. Sandboxes have their own event stream, covered under Sandbox events.

Events

Each delivery carries the agent's external_id and an end_user object (or null), so you route it to your own customer without a lookup. These are all 14 events; a subscription with no events listed gets every one.

Event Fires when
agent.deployed the agent finished deploying and is ready
agent.error the agent entered an error state. A failed build is told apart from other failures by data.failure_kind
agent.sleeping the agent auto-slept after going idle
agent.woken a sleeping agent was woken
agent.restarted the agent was restarted
agent.stopped the agent was stopped
agent.llm_limit_reached the agent reached its monthly AI spend limit; its AI calls stop until the next month or a higher limit. Sent once per agent per month
message.reply an async API message got its reply
message.failed an async API message failed
exec.finished a detached exec reached a final status. data has execution_id, status and exit_code. status "lost" means the result was gone or never arrived before the timeout
end_user.created an end user was created, by upsert or by createAgent
end_user.suspended an end user was suspended
end_user.resumed a suspended end user was resumed
end_user.deleted an end user was deleted with their agents

Subscribe

Create a subscription once, from the SDK below or from the dashboard under Settings → Webhooks. The signing secret is returned once. Store it to verify deliveries. Endpoints must be public https:// URLs.

TypeScriptPython

const hook = await maritime.webhooks.create({
  url: 'https://your-app.com/maritime/webhook',
  events: ['agent.error', 'agent.deployed'],   // omit for all events
})
// hook.secret is shown once. Store it to verify signatures.
await maritime.webhooks.test(hook.id)          // deliver a synthetic ping

Verify the signature

Every delivery is signed. Verify the X-Maritime-Signature header (an HMAC-SHA256 of the raw body with your secret, as sha256=<hex>) before trusting the payload. Both SDKs export a verifier that compares in constant time. Fail closed if the secret is unset. Never verify against an empty key.

TypeScriptPython

import { verifyWebhookSignature } from 'maritime-sdk'

const SECRET = process.env.MARITIME_WEBHOOK_SECRET
if (!SECRET) throw new Error('MARITIME_WEBHOOK_SECRET is required')

export async function POST(req: Request) {
  const raw = await req.text()  // exact bytes: the signature is over these
  const ok = await verifyWebhookSignature({
    secret: SECRET,
    body: raw,
    signature: req.headers.get('x-maritime-signature'),
  })
  if (!ok) return new Response('bad signature', { status: 401 })
  const event = JSON.parse(raw)
  // { event: 'agent.error', agent_id, external_id, end_user, timestamp, data: {...} }
  console.log(event.event, 'for your customer', event.external_id)
  return new Response('ok')
}

Delivery and retries

Each delivery carries these headers:

A delivery that does not get a 2xx is retried 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours later (7 attempts over about 33 hours), then marked dead. Dedupe on the delivery id, since a retry can arrive after your handler already acted. webhooks.deliveries shows the log of a subscription with each delivery's status, attempts and last error, and webhooks.replay sends one again now.

TypeScriptPython

const dead = await maritime.webhooks.deliveries(hook.id, { status: 'dead', limit: 50 })
for (const d of dead) await maritime.webhooks.replay(d.id)

An endpoint that fails 20 times in a row is disabled and gets no more deliveries. maritime.webhooks.list() shows isActive for each. Turn one back on in the dashboard under Settings → Webhooks, or with PATCH /api/v1/webhooks/{id} and the body {"isActive": true}, which also resets the failure count.

Sandbox events

Sandboxes send their own events to separate endpoints: sandbox.created, sandbox.running, sandbox.paused, sandbox.resumed, sandbox.ended, sandbox.pause_failed and sandbox.error. Register an endpoint with maritime.sandboxWebhooks (at least one event is required). These deliveries are signed with a Maritime-Signature: t=<unix seconds>,v1=<hex> header, an HMAC-SHA256 of <t>.<raw body>; the verifier refuses a timestamp more than 5 minutes off. Deliveries are kept for 30 days.

TypeScriptPython

const endpoint = await maritime.sandboxWebhooks.create({
  url: 'https://your-app.com/maritime/sandbox-events',
  events: ['sandbox.ended', 'sandbox.error'],
})
// endpoint.secret is shown once.

// In the receiver:
const ok = await maritime.sandboxWebhooks.verifySignature(raw, req.headers.get('maritime-signature'), SECRET)
if (ok) {
  const event = maritime.sandboxWebhooks.parseEvent(raw)  // { id, type, createdAt, sandboxId, endUser, data }
}

Runnable receivers

Full webhook receivers (Express + Flask) that verify the signature are in the examples.