Webhooks

Get a signed event on your server when a generation finishes.

We send a webhook to your server when a generation finishes, so you don't have to poll. Webhooks follow Standard Webhooks, so ready-made libraries can verify them.

Set up your endpoint

  • One endpoint per environment. Test and live each have their own https endpoint and their own signing secret, which starts with whsec_.
  • You set them in the dashboard, which opens with your invitation. There you can also send a test event.
  • Test jobs send real, signed events to your test endpoint. Test keys can also send webhook_url with a request, to send that job's events somewhere else.

Events

EventWhen
generation.succeededA generation finished. data is the generation, with output.
generation.failedA generation failed and its credit was refunded. data is the generation, and data.error.code says why.
account.low_balanceA job took your live credits below your low-balance threshold. data holds your account id and live credits, as in GET /v1/account.
webhook.testYou sent a test event from the dashboard. data holds a test message.

A generation.succeeded event (shortened here; data is the whole generation):

JSON
{
  "id": "evt_4Kq9xT2mVb7Zp1Ls8Hc3Rw",
  "type": "generation.succeeded",
  "environment": "live",
  "created_at": "2026-10-18T10:42:42.901Z",
  "data": {
    "id": "gen_4Kq9xT2mVb7Zp1Ls8Hc3Rw",
    "object": "generation",
    "status": "succeeded",
    "metadata": { "dish_id": "d_456" },
    "output": {
      "url": "https://api.foodshot.ai/v1/files/gen_4Kq9xT2mVb7Zp1Ls8Hc3Rw/image.jpg?exp=1792406563&sig=8fwqDeaHcFOU4b68TzesgCYKN8NG02QP3yMTmmVM9Pc",
      "content_type": "image/jpeg"
    }
  }
}

We may add event types. Answer 2xx to types you don't know, and ignore them.

Verify every webhook

Check the signature before you trust an event. The easiest way is a Standard Webhooks library, which takes your whsec_ secret as it is:

// npm install standardwebhooks
import { Webhook } from 'standardwebhooks';

const wh = new Webhook(process.env.FOODSHOT_WEBHOOK_SECRET);
// In your handler. rawBody: the exact bytes we sent. Throws if the signature or the timestamp is wrong.
const event = wh.verify(rawBody, req.headers);

Then:

  1. Answer 2xx within 10 seconds. Store the event and do the slow work, such as downloading the image, afterwards.
  2. Handle each event once. Skip a webhook-id you have already handled.

Retries

  • If your endpoint doesn't answer 2xx within 10 seconds, we try again for about 24 hours. Redirects count as failures.
  • Events can arrive twice or out of order. The generation's status is the truth, and succeeded and failed never change.
  • The generation's delivery field shows how delivery is going. If your endpoint was down, list your generations to catch up.

Rotate your secret

Rotate the secret in the dashboard whenever you need to. For 24 hours after a rotation, each event is signed with both the old and the new secret, so you can switch without losing events.

Verify without a library

Headers. webhook-id is the event id, the same on every retry. webhook-timestamp is when the attempt was signed, in Unix seconds. webhook-signature holds one or more signatures, such as v1,pEOl..., separated by spaces.

  1. Join webhook-id, webhook-timestamp and the raw body with dots:
    Text
    {webhook-id}.{webhook-timestamp}.{raw body}
  2. Sign that with HMAC-SHA256. The key is the part of your secret after whsec_, base64-decoded. Base64-encode the result.
  3. Accept the event if the result matches one of the v1, signatures in webhook-signature. Compare in constant time.
  4. Reject the event if webhook-timestamp is more than 5 minutes from your clock.

Test vector. Your verifier must accept this request. Its timestamp is in the past, so use 1792504800 as the current time when you test with it.

FieldValue
Secretwhsec_KBxnq+TLsMLw5CC0ynnKsGVMFsqFVFn4UNbIM5cfXuw= (an example)
webhook-idevt_7Hn2Wq5Rk8Ys1Df4Gb6Jt3Lm
webhook-timestamp1792504800
webhook-signaturev1,pEOlmetTrn5MsK0vNECj0TycJro/N/il4L4HEaOM08Q=

The raw body, on one line with no line break at the end:

JSON
{"id":"evt_7Hn2Wq5Rk8Ys1Df4Gb6Jt3Lm","type":"webhook.test","environment":"test","created_at":"2026-10-20T14:00:00.000Z","data":{"object":"webhook_test","message":"This is a test event from FoodShot."}}

IP addresses. Our webhooks come from addresses that change. Verify signatures instead of allowlisting IPs.

On this page