Docs · Integrations

Webhooks

Get an HTTPS POST when a sandbox becomes running, stops, fails or is deleted, instead of polling.

$ froog webhooks add https://example.com/froog --name prod       # all events; prints the signing secret once
$ froog webhooks add https://example.com/froog --event sandbox.error
$ froog webhooks test wh_…     # send a ping now
$ froog webhooks update wh_… --event sandbox.stopped --disable   # or --url, --name, --enable
$ froog webhooks deliveries wh_…   # recent attempts and their results
$ froog webhooks list · rotate wh_… · rm wh_…

Events: sandbox.running, sandbox.stopped, sandbox.error, sandbox.deleted, sandbox.expired (a stopped sandbox's disk was deleted after 14 days), sandbox.degraded (its host stopped answering, its VM stopped and is being started again so processes may have ended, or it isn't answering), sandbox.recovered (also when an errored sandbox was rebuilt), sandbox.stop_refused (a stop's snapshot failed, see Stopping safely), sandbox.disk_low (its disk is 85% or 95% full, see Disk space), and sandbox.port_opened / sandbox.port_closed (a TCP port started or stopped listening inside it. The body adds port and protocol). Port events go only to endpoints that list them in events, not to those that take every event: froog checks the running sandboxes of such accounts every 30 seconds, so a port that opens and closes in between isn't seen. For every change as it happens, use the port stream. CI job events, also only to endpoints that list them: ci.job.started, ci.job.completed (any conclusion, with what the job used and its test totals), ci.job.failed (failed or timed out on GitHub, or froog couldn't run it), ci.job.refused, and ci.monitor.triggered (a CI monitor fired; the body adds monitor and reason); their body is {"id", "type", "created_at", "ci_job": {…}} with the job as GET /ci/jobs/{id} returns it. The body is {"id", "type", "created_at", "sandbox": {…}, "reason"?}, reason saying why for degraded, recovered, stop_refused and disk_low, with the sandbox as GET /sandboxes/{name} returns it. Answer with any 2xx status within 10 seconds. Anything else is tried again, up to 8 attempts in all: after 5 seconds, 30 seconds, 2, 10 and 30 minutes, 1 hour and 2 hours. An event can arrive more than once and in no guaranteed order, so deduplicate on id. Delivery is at least once: pending retries are stored, so a restart of the control plane resumes them on schedule.

Requests also carry froog-event (the type), froog-delivery (the event id, the same on every attempt), froog-attempt (1 to 8) and froog-webhook (the endpoint's id). Every attempt, test pings included, goes in the endpoint's delivery log for 30 days, with its result, HTTP status and time: froog webhooks deliveries, GET /v1/webhooks/{id}/deliveries, or Deliveries in the dashboard. A paused endpoint (enabled: false) gets no events until you turn it back on.

Every request carries froog-signature: t=<unix time>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with your signing secret. Check it, and reject old timestamps:

// Node.js
const crypto = require('crypto');
function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const want = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return Math.abs(Date.now() / 1000 - Number(t)) < 300 &&
    crypto.timingSafeEqual(Buffer.from(want), Buffer.from(v1));
}

Endpoints must be HTTPS on port 443 or 8443 at a public address, and redirects aren't followed. Up to 10 per account.