# Webhooks

Receive signed, retried upload verdict events from Safeput.

Safeput can deliver a terminal upload verdict to one HTTPS endpoint per project:

```text
upload.clean
upload.rejected
upload.failed
```

Configure the URL and subscribed events from the dashboard. The signing secret
is displayed only when the endpoint is created or its secret is rotated.

## Event envelope

Each request is a JSON `POST` with an immutable event ID:

```json
{
  "id": "12c09ac1-dc57-4e4f-8b43-35f7a5fdf229",
  "type": "upload.clean",
  "api_version": "v1",
  "created_at": "2026-08-22T12:00:00.000Z",
  "data": {
    "upload": {
      "id": "7cc816f4-0e2e-4be0-9eaa-b9615a27ee52",
      "project_id": "47db67f6-8fbc-4c59-95eb-f5150e18d820",
      "status": "clean",
      "filename": "invoice.pdf",
      "verdict": { "result": "clean", "reason_code": null, "detected_type": "application/pdf" }
    }
  }
}
```

Use the event `id` as an idempotency key. Safeput uses at-least-once delivery,
so the same event can be received more than once.

## Verify the signature

Webhook requests include:

```http
Safeput-Event-Id: ...
Safeput-Event-Type: upload.clean
Safeput-Timestamp: 1787400000
Safeput-Signature: v1=<base64url-hmac>
```

Calculate HMAC-SHA256 with the webhook signing secret over the exact string
`<Safeput-Timestamp>.<raw request body>`. Base64url-encode the digest without
padding, prepend `v1=`, and compare it in constant time. Reject stale timestamps
before processing the event.

## Delivery behavior

Safeput accepts any `2xx` response. Network errors, timeouts, `408`, `429`, and
`5xx` responses retry with increasing delays for up to eight attempts. Redirects
are not followed. Other `3xx` and `4xx` responses fail permanently and can be
retried manually from the dashboard.

The receiver must answer within 10 seconds. Return success only after your
application has durably recorded the event.
