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

# Callbacks

> Receive automatic webhook notifications when interactive requests complete.

## Overview

Instead of polling the [status endpoint](/api-reference/check-status), you can provide a `callback_url` in your interactive notification request. NextKS will POST the results to your URL when the request finishes.

## When callbacks fire

A callback is triggered when:

* **`onUserAction` mode**: All users have responded
* **`onTimeout` mode**: The timeout is reached
* **Either mode**: The timeout is reached and some users haven't responded

A callback fires **once**, only when the request reaches its final state (`status: "finished"`). It is never sent for partial or intermediate individual responses — the payload always reflects the completed request.

## Signature verification

Every callback request is signed with HMAC-SHA256 so you can verify it came from NextKS.

When you send an interactive notification with a `callback_url`, the API response includes a `callback_secret`. Store this secret securely — you'll use it to verify incoming callbacks.

### Verification headers

| Header                | Description                                                                             |
| --------------------- | --------------------------------------------------------------------------------------- |
| `X-NextKS-Signature`  | `sha256=<hex>` — HMAC-SHA256 of the raw request body, keyed with your `callback_secret` |
| `X-NextKS-Request-Id` | The `request_id` from the original notification                                         |

### Verification example

<CodeGroup>
  ```typescript Node.js theme={null}
  import crypto from 'crypto'

  function verifyCallback(body: string, signature: string, secret: string): boolean {
    const expected = 'sha256=' + crypto
      .createHmac('sha256', secret)
      .update(body)
      .digest('hex')
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_callback(request, callback_secret: str) -> bool:
      signature = request.headers.get("X-NextKS-Signature", "")
      expected = "sha256=" + hmac.new(
          callback_secret.encode(),
          request.data,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)
  ```
</CodeGroup>

## Callback payload

NextKS sends a `POST` request with `Content-Type: application/json`:

```json theme={null}
{
  "request_id": "req_a1b2c3d4e5f6",
  "external_reference_id": "deploy-v2.4.1",
  "status": "finished",
  "responses": [
    {
      "user_email": "alice@company.com",
      "option_value": "approve",
      "timestamp": "2026-02-11T14:23:01.000Z",
      "expired": false
    },
    {
      "user_email": "bob@company.com",
      "option_value": null,
      "timestamp": null,
      "expired": true
    }
  ]
}
```

## Callback URL requirements

Your callback URL is validated when you submit the notification:

| Requirement  | Details                                                  |
| ------------ | -------------------------------------------------------- |
| Protocol     | HTTPS only                                               |
| DNS          | Must resolve to a public IP address                      |
| Reachability | Must respond to a HEAD request within 5 seconds          |
| Private IPs  | Blocked (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x) |

If validation fails, the notification request is rejected with a `400` error.

## Retries

If a callback delivery fails (network error or non-2xx response), NextKS retries automatically with exponential backoff:

| Attempt     | Delay after failure | Total elapsed |
| ----------- | ------------------- | ------------- |
| 1 (initial) | immediate           | 0             |
| 2           | 1 minute            | \~1 min       |
| 3           | 2 minutes           | \~3 min       |
| 4           | 4 minutes           | \~7 min       |
| 5           | 8 minutes           | \~15 min      |
| 6           | 16 minutes          | \~31 min      |

* Up to **6 total attempts** (1 initial + 5 retries)
* Each attempt is logged with the error message
* Once successful, no further retries are attempted

## Handling callbacks

Your endpoint should:

1. Return a `2xx` status code to acknowledge receipt
2. Process the payload asynchronously if needed — NextKS has a 10-second timeout
3. Be idempotent — in rare cases, a callback may be delivered more than once

<CodeGroup>
  ```typescript Node.js (Express) theme={null}
  import crypto from 'crypto'
  import express from 'express'

  const app = express()

  // Store secrets when creating notifications: { requestId: callbackSecret }
  const callbackSecrets: Record<string, string> = {}

  app.post('/hooks/nextks', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-nextks-signature'] as string ?? ''
    const requestId = req.headers['x-nextks-request-id'] as string ?? ''
    const secret = callbackSecrets[requestId]

    // Verify signature
    if (secret) {
      const expected = 'sha256=' + crypto
        .createHmac('sha256', secret)
        .update(req.body)
        .digest('hex')
      if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
        return res.status(403).send('Invalid signature')
      }
    }

    const data = JSON.parse(req.body.toString())
    const approved = data.responses
      .filter((r: any) => !r.expired)
      .every((r: any) => r.option_value === 'approve')

    if (approved) {
      triggerDeployment(data.external_reference_id)
    }

    res.sendStatus(200)
  })
  ```

  ```python Python (Flask) theme={null}
  import hmac
  import hashlib
  from flask import Flask, request

  app = Flask(__name__)

  # Store secrets when creating notifications: { request_id: callback_secret }
  CALLBACK_SECRETS = {}

  @app.route("/hooks/nextks", methods=["POST"])
  def handle_nextks_callback():
      signature = request.headers.get("X-NextKS-Signature", "")
      request_id = request.headers.get("X-NextKS-Request-Id", "")
      secret = CALLBACK_SECRETS.get(request_id)

      # Verify signature
      if secret:
          expected = "sha256=" + hmac.new(
              secret.encode(), request.data, hashlib.sha256
          ).hexdigest()
          if not hmac.compare_digest(signature, expected):
              return "Invalid signature", 403

      data = request.json
      approved = all(
          r["option_value"] == "approve"
          for r in data["responses"]
          if not r["expired"]
      )

      if approved:
          trigger_deployment(data["external_reference_id"])

      return "", 200
  ```
</CodeGroup>
