webhooks.md
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.
TypeScript
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
Python
hook = client.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.
client.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.
TypeScript
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')
}
Python
import json, os
from maritime import verify_webhook_signature
SECRET = os.environ["MARITIME_WEBHOOK_SECRET"] # fails closed when unset
@app.post("/maritime/webhook")
async def hook(request: Request):
raw = await request.body() # exact bytes: the signature is over these
if not verify_webhook_signature(SECRET, raw, request.headers.get("X-Maritime-Signature")):
return Response(status_code=401)
event = json.loads(raw)
print(event["event"], "for your customer", event["external_id"])
return Response("ok")
Delivery and retries
Each delivery carries these headers:
X-Maritime-Event: the event nameX-Maritime-Delivery: the delivery id, the same on every retryX-Maritime-Attempt: 1 on the first try, then 2, 3 and so onX-Maritime-Signature:sha256=<hex>over the raw bodywebhook-id,webhook-timestampandwebhook-signature: the same delivery signed by the Standard Webhooks spec (v1,<base64>overid.timestamp.body), so a receiver can dedupe on the id and refuse an old timestamp
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.
TypeScript
const dead = await maritime.webhooks.deliveries(hook.id, { status: 'dead', limit: 50 })
for (const d of dead) await maritime.webhooks.replay(d.id)
Python
dead = client.webhooks.deliveries(hook["id"], status="dead", limit=50)
for d in dead:
client.webhooks.replay(d["id"])
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.
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 }
}
endpoint = client.sandbox_webhooks.create(
url="https://your-app.com/maritime/sandbox-events",
events=["sandbox.ended", "sandbox.error"],
)
# endpoint.secret is shown once.
# In the receiver:
if client.sandbox_webhooks.verify_signature(raw, request.headers.get("Maritime-Signature"), SECRET):
event = client.sandbox_webhooks.parse_event(raw) # event.type, event.sandbox_id, event.data
Runnable receivers
Full webhook receivers (Express + Flask) that verify the signature are in the examples.