SDK

Webhooks

Get notified when a customer's agent changes state, instead of polling. Subscribe a URL, and Maritime POSTs a signed JSON body on every lifecycle event.

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.

EventFires when
agent.deployedthe agent finished deploying and is ready
agent.errorthe agent entered an error state. A failed build is told apart from other failures by data.failure_kind
agent.sleepingthe agent auto-slept after going idle
agent.wokena sleeping agent was woken
agent.restartedthe agent was restarted
agent.stoppedthe agent was stopped
agent.llm_limit_reachedthe 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.replyan async API message got its reply
message.failedan async API message failed
exec.finisheda 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.createdan end user was created, by upsert or by createAgent
end_user.suspendedan end user was suspended
end_user.resumeda suspended end user was resumed
end_user.deletedan 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.

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.

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:

  • X-Maritime-Event: the event name
  • X-Maritime-Delivery: the delivery id, the same on every retry
  • X-Maritime-Attempt: 1 on the first try, then 2, 3 and so on
  • X-Maritime-Signature: sha256=<hex> over the raw body
  • webhook-id, webhook-timestamp and webhook-signature: the same delivery signed by the Standard Webhooks spec (v1,<base64> over id.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.

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.

Runnable receivers

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