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

# Webhooks: Receive and Verify Async Brand Generation Results

> Configure and verify Nomiq webhooks to receive generation.success and generation.failed events with HMAC-SHA256 cryptographic signature validation.

Because brand generation is handled asynchronously by a computationally intensive AI Engine — taking between 3 and 10 seconds per job — the `POST /v1/brands/generate` endpoint returns a `202 Accepted` immediately without the final assets. Nomiq delivers those assets by sending an HTTP `POST` request to your configured webhook endpoint the moment a job completes or fails. You must implement a webhook receiver to build a fully functional integration.

## Event Types

Nomiq dispatches two event types for generation jobs. Every webhook request body contains a top-level `type` field you can use to route handling logic.

| Event Type           | Trigger                                                                                       | Payload                                                      |
| -------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `generation.success` | The AI Engine successfully compiled all SVG assets and CSS variables for the job.             | Complete Brand Asset object including SVGs and style tokens. |
| `generation.failed`  | The AI Engine encountered a fatal, unrecoverable error after exhausting all internal retries. | The originating job ID and a human-readable error reason.    |

***

## Example Webhook Payload

The following shows a representative `generation.success` payload. The `data.assets` object contains the deliverables you requested from the Generation API.

```json theme={null}
{
  "event_id": "evt_5k6l7m8n",
  "type": "generation.success",
  "created_at": 1677649428,
  "data": {
    "job_id": "job_9x8y7z6w",
    "project_id": "proj_1A2b3C4d5E",
    "memory_id": "mem_X1y2Z3w4",
    "assets": {
      "logo_svg": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>...</svg>",
      "logo_svg_dark": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>...</svg>",
      "primary_color": "#4338ca",
      "secondary_color": "#faf7f0",
      "font_family": "Inter",
      "font_weight_heading": "700",
      "font_weight_body": "400"
    }
  }
}
```

A `generation.failed` payload omits the `assets` object and instead includes an `error` field:

```json theme={null}
{
  "event_id": "evt_2p3q4r5s",
  "type": "generation.failed",
  "created_at": 1677649435,
  "data": {
    "job_id": "job_9x8y7z6w",
    "project_id": "proj_1A2b3C4d5E",
    "error": {
      "code": "generation_timeout",
      "message": "The AI Engine did not produce a result within the maximum allowed processing window."
    }
  }
}
```

***

## Security & Signature Verification

Every webhook request Nomiq sends includes a `Nomiq-Signature` header containing an HMAC-SHA256 hex digest of the raw request body, signed with your unique Webhook Secret. You must verify this signature on every incoming request before processing the payload — without it, a malicious actor could POST arbitrary data to your endpoint.

<Steps>
  <Step title="Retrieve Your Webhook Secret">
    Navigate to **Workspace Settings → Webhooks** in the Nomiq Dashboard. Your Webhook Secret is displayed here (format: `whsec_abc123`). This secret is distinct from your API key — do not expose it in client-side code or commit it to source control. Store it as an environment variable (e.g., `NOMIQ_WEBHOOK_SECRET`).
  </Step>

  <Step title="Compute the Expected HMAC-SHA256 Signature">
    Using the **raw, unparsed request body bytes** (not a re-serialized JSON object), compute an HMAC-SHA256 digest keyed on your Webhook Secret. Compare the resulting hex string to the value in the `Nomiq-Signature` header. Ensure you parse the body as raw bytes — parsing to JSON and re-serializing can alter whitespace and break the signature check.

    <CodeGroup>
      ```javascript Node.js (Express) theme={null}
      import express from "express";
      import crypto from "crypto";

      const app = express();
      const webhookSecret = process.env.NOMIQ_WEBHOOK_SECRET; // "whsec_abc123"

      // IMPORTANT: Use express.raw() so the body is not parsed.
      // JSON body-parser middleware will break signature verification.
      app.post(
        "/api/nomiq/webhook",
        express.raw({ type: "application/json" }),
        (req, res) => {
          const receivedSig = req.headers["nomiq-signature"];
          const rawBody = req.body; // Buffer

          const expectedSig = crypto
            .createHmac("sha256", webhookSecret)
            .update(rawBody)
            .digest("hex");

          if (
            !crypto.timingSafeEqual(
              Buffer.from(receivedSig, "hex"),
              Buffer.from(expectedSig, "hex")
            )
          ) {
            console.error("Webhook signature mismatch — request rejected.");
            return res.status(400).send("Invalid signature");
          }

          // Signature verified — safe to process the payload
          const event = JSON.parse(rawBody.toString("utf8"));
          console.log("Verified event type:", event.type);

          // Queue heavy processing asynchronously; return 200 immediately
          res.status(200).send();
        }
      );
      ```

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

      app = Flask(__name__)
      webhook_secret = os.environ.get("NOMIQ_WEBHOOK_SECRET")  # "whsec_abc123"


      @app.route("/api/nomiq/webhook", methods=["POST"])
      def nomiq_webhook():
          received_sig = request.headers.get("Nomiq-Signature", "")
          raw_body = request.get_data()  # raw bytes — do NOT use request.json here

          expected_sig = hmac.new(
              webhook_secret.encode("utf-8"),
              raw_body,
              hashlib.sha256,
          ).hexdigest()

          if not hmac.compare_digest(expected_sig, received_sig):
              return jsonify(error="Invalid signature"), 400

          # Signature verified — safe to process the payload
          event = json.loads(raw_body)
          print("Verified event type:", event["type"])

          # Queue heavy processing asynchronously; return 200 immediately
          return jsonify(success=True), 200
      ```
    </CodeGroup>
  </Step>

  <Step title="Compare Signatures Using a Constant-Time Function">
    Always use a **constant-time comparison** function (such as `crypto.timingSafeEqual` in Node.js or `hmac.compare_digest` in Python) when checking the signatures. Standard string equality operators (`===`, `==`) are vulnerable to timing attacks that could allow an attacker to forge a valid signature through repeated probing.
  </Step>
</Steps>

***

## Retry Behavior

Nomiq expects your endpoint to respond with an HTTP `2xx` status code within **5 seconds**. If your server returns a `4xx` or `5xx` response, or does not respond within the timeout window, Nomiq treats the delivery as failed and initiates an exponential backoff retry strategy. After four consecutive failures, the event is marked as undeliverable and no further retries are attempted.

<Frame>
  ```mermaid theme={null}
  flowchart LR
      A[Initial Attempt] -->|Fails or times out| B(Wait 1 min)
      B --> C[Attempt 2]
      C -->|Fails or times out| D(Wait 5 mins)
      D --> E[Attempt 3]
      E -->|Fails or times out| F(Wait 30 mins)
      F --> G[Attempt 4]
      G -->|Fails or times out| H[Marked Undeliverable]
      A -->|2xx received| S[Delivered ✓]
      C -->|2xx received| S
      E -->|2xx received| S
      G -->|2xx received| S
  ```
</Frame>

<Warning>
  **Return `200 OK` immediately — do not perform heavy processing before responding.** Database writes, image transforms, and third-party API calls can easily exceed the 5-second timeout and trigger unwanted retries, causing duplicate processing on your end. Instead, persist the raw payload to a queue (e.g., Redis, BullMQ, SQS) inside your webhook handler, return `200` at once, and process the queued event in a background worker.
</Warning>

***

## Testing Webhooks Locally

Before deploying to production, you can verify your webhook handler works correctly by forwarding Nomiq's test events to your local development server.

<Steps>
  <Step title="Expose Your Local Server">
    Use a tunneling tool such as [ngrok](https://ngrok.com) or [localtunnel](https://theboroer.github.io/localtunnel-www/) to create a publicly reachable HTTPS URL pointing at your local server. For example:

    ```bash theme={null}
    ngrok http 3000
    # Forwarding: https://a1b2-203-0-113-42.ngrok-free.app -> http://localhost:3000
    ```

    Copy the generated HTTPS URL (e.g., `https://a1b2-203-0-113-42.ngrok-free.app`).
  </Step>

  <Step title="Register the Endpoint in Nomiq">
    In the Nomiq Dashboard, navigate to **Workspace Settings → Webhooks → Add Endpoint**. Paste your full webhook URL (e.g., `https://a1b2-203-0-113-42.ngrok-free.app/api/nomiq/webhook`) and save it. Nomiq will display your Webhook Secret on this page — copy it into your environment variables before starting your local server.
  </Step>

  <Step title="Send a Test Event">
    On the Webhooks settings page, click **Send Test Event** next to your registered endpoint. Nomiq dispatches a synthetic `generation.success` payload to your URL with a valid `Nomiq-Signature` header. Check your local server logs to confirm the payload was received and the signature was verified correctly. If the signature check fails, ensure you are reading the **raw** request body bytes rather than a parsed JSON object.
  </Step>
</Steps>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Generation API" icon="bolt" href="/api-reference/generation">
    Dispatch the async brand generation jobs whose results you'll receive here via webhook.
  </Card>

  <Card title="Errors" icon="circle-exclamation" href="/api-reference/errors">
    Review all possible error codes and HTTP status responses you may encounter in your integration.
  </Card>
</CardGroup>
