Skip to content

Results back to your app

Instead of asking for results, have Sight send them. Set one https address for your account; every time one of your jobs is checked (the fast answer, then the final one), Sight posts the result there, signed.

Set the address

const { secret } = await checks.setWebhook("https://your-app.example/sight-results");
// Keep `secret`: it is shown only now. Store it as, say, SIGHT_WEBHOOK_SECRET.

Only a key allowed to manage the webhook

Whoever can change this address receives every later result. So only a key an owner or admin of your company has allowed to manage the webhook (in the console: Company → API keys) can set, read or remove it; any other key gets 403. Every change is recorded with the key that made it. Keep that key on your server, apart from any key that only sends photos.

Setting the address again replaces it and its secret. Results still waiting for the old address are dropped, never sent to the new one.

Receive and verify

Check the signature on the raw body before trusting anything in it:

import express from "express";
import { verifyWebhook } from "@raku-technologies/sight";

app.post("/sight-results", express.raw({ type: "application/json" }), async (req, res) => {
  const event = await verifyWebhook(req.body, req.get("x-sight-signature"), process.env.SIGHT_WEBHOOK_SECRET!);
  res.sendStatus(204);                        // answer at once; do the work afterwards
  await saveResult(event.id, event.job, event.final);
});

verifyWebhook throws if the signature does not match, the body was changed, or the delivery is more than five minutes old.

What is sent

POST <your address>
Content-Type: application/json
X-Sight-Event:     job.checked
X-Sight-Delivery:  <delivery id>
X-Sight-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your secret>

{ "id": "<delivery id>", "event": "job.checked", "final": true,
  "checked_at": "2026-10-03T05:00:00.000Z", "job": { ...the job, as GET returns it... } }

Without the SDK, compute HMAC-SHA256 over <t>.<raw body> with your secret, compare it to v1 in constant time, and refuse a t more than five minutes from now.

Rules your receiver can rely on

  • final: false is the fast answer (checks waiting for a reading say pending); a later delivery has the final one. Deliveries can arrive out of order after retries: keep the one with the latest checked_at.
  • Retries: anything but a 2xx answer is retried with growing waits (15 s, 30 s, 1 min, … up to an hour), 8 tries in all. id stays the same on a retry: use it to ignore repeats. It is inside the signed body, so it cannot be changed.
  • Answer within 15 seconds. Do the work after answering. Your answer's body is never read.
  • Your address must be public https, with no username or password in it. Sight connects to the IP it checked, so the name cannot be pointed elsewhere in between, and does not follow redirects.
  • Evidence names your own photos (external_photo_id), so a result can be put on the right photo or form question.

When one seems missing

const { deliveries } = await checks.deliveries(jobId);
// each: state (queued, sending, sent, failed), attempts, last_status, last_error

The photo with its boxes drawn on

To keep a copy of a photo with what the checks found drawn on it (each box, labelled, green / red / amber by outcome) in your own storage:

An illustration of a drawn photo: each box labelled, green for pass, amber for needs review

An illustration on a made-up scene. Green means passed, amber needs review, red failed. Labels never cover each other.

const jpeg = await checks.annotatedPhoto(jobId, photoId);   // Uint8Array, JPEG
await s3.putObject({ Bucket, Key: `checked/${photoId}.jpg`, Body: jpeg, ContentType: "image/jpeg" });

For a photo sent with detect, only those things are drawn, each with its score: green when confident, amber otherwise.