> ## Documentation Index
> Fetch the complete documentation index at: https://docs.samsa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Ricevi una callback firmata quando un job finisce, verifica la firma e gestisci i retry.

Ogni endpoint di generazione è asincrono. Invece di interrogare l'endpoint `GET`,
puoi passare un **`webhook_url`** e Samsa farà un `POST` di un evento firmato a esso
nel momento in cui il job raggiunge uno stato terminale.

## Richiedere un webhook

Aggiungi `webhook_url` a qualsiasi richiesta di generazione. È **per richiesta** — job
diversi possono puntare a URL diversi.

```bash theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/generations \
  -H "Authorization: Bearer $SAMSA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A ceramic mug on linen, soft daylight",
    "webhook_url": "https://api.example.com/hooks/samsa"
  }'
```

<Note>
  I webhook sono una **comodità, non la fonte di verità**. Se ogni tentativo di
  consegna fallisce, il risultato del job è comunque disponibile dal suo endpoint
  `GET`. Interroga come fallback per qualsiasi cosa critica.
</Note>

## Eventi

I webhook si attivano solo sulle transizioni **terminali** — `completed` o `failed`.
I job `cancelled` **non** emettono un webhook.

| Evento                                                   | Si attiva quando                                           |
| -------------------------------------------------------- | ---------------------------------------------------------- |
| `image.generation.completed` / `image.generation.failed` | Una generazione di immagine raggiunge uno stato terminale. |
| `image.edit.completed` / `image.edit.failed`             | Un Magic Edit raggiunge uno stato terminale.               |
| `video.generation.completed` / `video.generation.failed` | Una generazione di video raggiunge uno stato terminale.    |
| `model.completed` / `model.failed`                       | Una creazione di modello raggiunge uno stato terminale.    |

## Payload

Il corpo della richiesta è JSON. L'oggetto `data` ha la stessa forma che ottieni
dall'endpoint di stato `GET` del job.

```json theme={null}
{
  "event": "image.generation.completed",
  "id": "7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40",
  "status": "completed",
  "organization_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "api_key_id": "9c8d7e6f-5a4b-4c3d-2e1f-0a9b8c7d6e5f",
  "created_at": "2026-07-02T12:00:00Z",
  "data": {
    "id": "7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40",
    "status": "completed",
    "created_at": "2026-07-02T12:00:00Z",
    "credits_used": 5,
    "images": [
      {
        "id": "d4c3b2a1-6f5e-4b3a-9d8c-1e0f2a3b4c5d",
        "url": "https://cdn.samsa.ai/user-.../7f9c0e2a.png?X-Amz-Signature=...",
        "width": 1024,
        "height": 1024,
        "seed": 128390
      }
    ],
    "error": null
  }
}
```

Per un evento `failed`, `status` è `"failed"` e `data` porta un oggetto `error` che
usa la stessa forma interna dell'[envelope di errore](/it/guides/errors#l-envelope-di-errore).

## Header di firma

Ogni consegna porta tre header:

```
webhook-id: msg_2n0jJ2mCw4T5qX1aB3cD4e
webhook-timestamp: 1782043200
webhook-signature: v1,F9epTMwALBtPM7ghiLNEmdmVN7TizCpZ+zAHLPwip9A=
```

| Header              | Descrizione                                                                                                    |
| ------------------- | -------------------------------------------------------------------------------------------------------------- |
| `webhook-id`        | Id univoco per l'evento, **stabile tra i retry** (usalo per deduplicare).                                      |
| `webhook-timestamp` | Secondi Unix di quando l'evento è stato inviato. Rifiuta se è a più di 300s da adesso (protezione dai replay). |
| `webhook-signature` | Lista separata da spazi di firme `v1,<base64>`. Verifica rispetto a **ciascun** candidato `v1,`.               |

Lo schema è compatibile con Svix: HMAC-SHA256 su
`{webhook-id}.{webhook-timestamp}.{raw-body}`, codificato in base64, con prefisso
`v1,`.

```
signed_input = utf8(f"{webhook_id}.{webhook_timestamp}.") + raw_body_bytes
key          = base64_decode(webhook_secret without the "whsec_" prefix)
signature    = "v1," + base64( HMAC_SHA256(key, signed_input) )
```

<Warning>
  Firma e verifica sui **byte grezzi della richiesta** esattamente come ricevuti — mai
  una copia ri-serializzata. La ri-serializzazione (riordinare le chiavi, cambiare gli
  spazi bianchi) cambia i byte e rompe la firma.
</Warning>

## Verificare la firma

Il tuo `webhook_secret` per chiave vive nell'app sotto **Settings → API Keys** (ogni
chiave ha il proprio segreto; ruotalo indipendentemente dalla chiave). Gli snippet qui
sotto sono verificati rispetto a un **vettore di test fisso** — eseguili così come sono
e restituiscono `true`, così puoi confermare che la tua implementazione riproduca la
firma byte per byte prima di collegare un segreto reale.

<Note>
  Il corpo del vettore di test è una stringa di riferimento fissa, quindi i suoi byte
  esatti non cambiano mai e la firma rimane riproducibile — intenzionalmente non è il
  payload dell'evento live mostrato [sopra](#payload). La verifica della firma gira
  sempre sui **byte grezzi esatti che ricevi**, qualunque sia la loro forma, quindi
  questo è puramente un self-test per il tuo verificatore. Allo stesso modo,
  `webhook-id` è una stringa opaca e stabile per evento — trattala come un token, non
  analizzarla mai.
</Note>

<CodeGroup>
  ```python Python theme={null}
  import base64
  import hashlib
  import hmac


  def verify(secret: str, webhook_id: str, timestamp: int, body: bytes, header: str) -> bool:
      # Accept both base64 alphabets + missing padding (real secrets are urlsafe/unpadded).
      token = secret.removeprefix("whsec_").replace("+", "-").replace("/", "_")
      key = base64.urlsafe_b64decode(token + "=" * (-len(token) % 4))
      signed_input = f"{webhook_id}.{timestamp}.".encode() + body
      expected = base64.b64encode(
          hmac.new(key, signed_input, hashlib.sha256).digest(),
      ).decode("ascii")
      valid = False
      for candidate in header.split(" "):
          if candidate.startswith("v1,"):
              valid = hmac.compare_digest(candidate[3:], expected) or valid
      return valid


  # Fixed test vector — any correct implementation reproduces this signature.
  assert verify(
      "whsec_c2FtcGxlLXNlY3JldC1kby1ub3QtdXNl",
      "msg_2n0jJ2mCw4T5qX1aB3cD4e",
      1782043200,
      b'{"type":"image.generation.completed","created_at":"2026-07-02T12:00:00Z",'
      b'"data":{"id":"7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40","object":"image.generation",'
      b'"status":"completed"}}',
      "v1,F9epTMwALBtPM7ghiLNEmdmVN7TizCpZ+zAHLPwip9A=",
  )
  ```

  ```typescript TypeScript theme={null}
  import crypto from "node:crypto";

  function verify(
    secret: string,
    webhookId: string,
    timestamp: number,
    body: Buffer,
    header: string,
  ): boolean {
    // Accept both base64 alphabets + missing padding (real secrets are urlsafe/unpadded).
    const token = secret.replace(/^whsec_/, "").replace(/-/g, "+").replace(/_/g, "/");
    const key = Buffer.from(token, "base64");
    const signedInput = Buffer.concat([
      Buffer.from(`${webhookId}.${timestamp}.`, "utf8"),
      body,
    ]);
    const expected = crypto.createHmac("sha256", key).update(signedInput).digest("base64");
    let valid = false;
    for (const candidate of header.split(" ")) {
      if (!candidate.startsWith("v1,")) continue;
      const sig = Buffer.from(candidate.slice(3));
      const exp = Buffer.from(expected);
      if (sig.length === exp.length && crypto.timingSafeEqual(sig, exp)) valid = true;
    }
    return valid;
  }

  // Fixed test vector — any correct implementation reproduces this signature.
  const ok = verify(
    "whsec_c2FtcGxlLXNlY3JldC1kby1ub3QtdXNl",
    "msg_2n0jJ2mCw4T5qX1aB3cD4e",
    1782043200,
    Buffer.from(
      '{"type":"image.generation.completed","created_at":"2026-07-02T12:00:00Z",' +
        '"data":{"id":"7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40","object":"image.generation",' +
        '"status":"completed"}}',
      "utf8",
    ),
    "v1,F9epTMwALBtPM7ghiLNEmdmVN7TizCpZ+zAHLPwip9A=",
  );
  if (!ok) throw new Error("signature verification failed");
  ```
</CodeGroup>

In produzione, applica sempre anche la **finestra del timestamp**: rifiuta la consegna
se `|now − webhook-timestamp| > 300` secondi.

## Retry e regole di consegna

* Il **successo** è qualsiasi risposta `2xx` restituita entro un timeout di **10
  secondi** (timeout di connessione 5s). Un `3xx` è trattato come un fallimento — Samsa
  **non** segue i redirect.
* In caso di fallimento, Samsa riprova con il tentativo iniziale più **5 retry** — **6
  tentativi di consegna in totale** — con back-off `5s → 30s → 2m → 15m → 1h`.
* Dopo il tentativo finale la consegna viene abbandonata (e registrata). Il risultato
  del job resta interrogabile dal suo endpoint `GET`.
* Gli endpoint devono essere **HTTPS**. Rispondi rapidamente (`2xx`) ed esegui
  l'elaborazione pesante in modo asincrono così non superi mai la finestra di 10
  secondi.

<Tip>
  Usa il `webhook-id` stabile per rendere il tuo handler **idempotente**. Una consegna
  ritentata riusa lo stesso `webhook-id`, così puoi ignorare in sicurezza un evento che
  hai già elaborato.
</Tip>
