Skip to content
SBDocs

Customers · Guides

Poll or get a webhook

Learn that a run has ended: read it every few seconds, or have Sushibelt call your HTTPS endpoint and verify its signature.

Before you start
  • An API key with the runs:create and runs:read scopes.
  • For webhooks: an HTTPS endpoint that Sushibelt can reach.
You'll end with
Your code hears when each run completes or fails, once, from a delivery it has verified.

A run takes from a few seconds to many minutes. You can ask how it is going (polling), or give Sushibelt a URL to call when it ends (a webhook). Polling is simpler and works anywhere; a webhook needs a public HTTPS endpoint but saves you the requests. Many integrations use both: a webhook to hear quickly, and a slow poll as a fallback.

Polling#

Read the run with Get a run until status is completed or failed.

  • A few seconds apart is enough. Start at two seconds and slow down to ten for long runs.
  • Mind the rate limit. A key may make 60 requests a minute by default, and every request counts, polling included. Several runs polled at once share that budget.
  • Set a time limit. A run ends as completed, failed, stopped or cancelled — but a limit keeps a slow provider from holding your code open.
  • Reading is free. It never spends credits, and a key that has reached its monthly cap can still read the runs it started.
Get a run
JOB_ID="YOUR_JOB_ID"

curl "https://app.sushibelt.ai/api/v1/runs/$JOB_ID" \
  -H "Authorization: Bearer $SUSHIBELT_API_KEY"

Run a workflow from code has a complete polling function in TypeScript and Python.

Webhooks#

Pass callbackUrl when you start a run, and Sushibelt sends a POST to it when the run completes or fails. The URL must use https://; anything else is refused with 422 before the run starts. Add a callbackSecret of your choosing and each delivery is signed with it.

Start a run with a webhook
WORKFLOW_ID="YOUR_WORKFLOW_ID"

curl -X POST "https://app.sushibelt.ai/api/v1/workflows/$WORKFLOW_ID/runs" \
  -H "Authorization: Bearer $SUSHIBELT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://example.com/hooks/sushibelt",
    "callbackSecret": "YOUR_WEBHOOK_SECRET"
  }'

What you receive#

The body is the same as Get a run, read at the moment of sending, plus event. The full schema is in the reference under Run finished.

Webhook headers
HeaderDescription
Content-Typeapplication/json.
X-Sushibelt-Eventrun.completed, run.failed, run.stopped or run.cancelled. The same value as event in the body.
X-Sushibelt-SignatureSent only when you passed callbackSecret: sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with your secret.
Example delivery bodyJSON
{
  "event": "run.completed",
  "jobId": "8b2d6f10-4c3a-4e59-a7d2-91f0c3e8b5a4",
  "status": "completed",
  "progress": 100,
  "steps": [ ... ],
  "output": [ ... ]
}

Decide what happened from event. There are four events: run.completed, run.failed, run.stopped when someone on your team stops the run in the app, and run.cancelled when someone cancels it.

How to answer, and retries#

  • Answer with any 2xx status within 8 seconds. Do slow work after you answer.
  • Anything else — another status, a timeout, a connection error — is retried. The first delivery is attempted shortly after the run ends; after a failure the next attempt waits 2 minutes, then twice as long each time (4, 8, 16 minutes), never more than an hour. After 5 attempts in all, Sushibelt stops trying.
  • A delivery can arrive twice. Treat jobId and event together as its idempotency key, and ignore a pair you have handled.
  • There is no log of deliveries to read back. If your endpoint was down for longer than the retries last, read the run with Get a run.

Verify the signature#

  1. Read the raw body, before any JSON parsing. Parsing and re-serialising changes the bytes, and the signature with them.
  2. Compute HMAC-SHA256 of the raw body, keyed with your callbackSecret, and hex-encode it.
  3. Compare it with the value after sha256= in X-Sushibelt-Signature, in constant time. If they differ, answer 401 and ignore the body.
A webhook receiver that verifies each delivery
// webhook.mts: Node 18 or later. Run with: npx tsx webhook.mts
import { createServer } from 'node:http'
import { createHmac, timingSafeEqual } from 'node:crypto'

const SECRET = process.env.SUSHIBELT_WEBHOOK_SECRET ?? ''
const seen = new Set<string>() // in production, keep this in your database

function signatureIsValid(rawBody: Buffer, header: string | undefined): boolean {
  if (!header?.startsWith('sha256=')) return false
  const expected = createHmac('sha256', SECRET).update(rawBody).digest('hex')
  const given = header.slice('sha256='.length)
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected))
}

createServer((req, res) => {
  const chunks: Buffer[] = []
  req.on('data', (chunk: Buffer) => chunks.push(chunk))
  req.on('end', () => {
    const raw = Buffer.concat(chunks) // verify the raw bytes, before parsing
    if (!signatureIsValid(raw, req.headers['x-sushibelt-signature'] as string | undefined)) {
      res.writeHead(401).end()
      return
    }
    res.writeHead(200).end() // answer at once; do slow work afterwards

    const delivery = JSON.parse(raw.toString('utf8'))
    const id = `${delivery.jobId}:${delivery.event}`
    if (seen.has(id)) return // a repeat of a delivery you already handled
    seen.add(id)
    console.log(`${delivery.event}: run ${delivery.jobId}`)
  })
}).listen(3000)

Warning: the secret travels with each run

Use a long random value, keep it in a secret store, and pass it only over HTTPS. Anyone with the secret can sign a body your endpoint will believe.

Next steps