[Webhook handling](#webhook-handling)
[1. Register webhook events](#1-register-webhook-events)
[2. Listen to webhook events](#2-listen-to-webhook-events)
[3. HMAC signature verification](#3-hmac-signature-verification)

---

!!!info
This is a guide to help you understand how your Quable App can register and listen to Quable PIM webhook events using Next.js API routes.
!!!

---

## Webhook handling

Webhooks are HTTP requests sent by the Quable PIM to your application to inform it about events in real-time. This enables seamless communication between your app and the PIM, allowing you to trigger actions as events occur (e.g., a document is created or updated).

---

### Requirements

We'll use the [`@quable/quable-pim-js`](https://www.npmjs.com/package/@quable/quable-pim-js) library to register webhooks via the Quable PIM API.

---

### 1. Register webhook events

#### Define webhook events

Open the `quable.app.yml` file at the root of your project and add the webhook events you want to listen to:

```yaml
application_type: in_app
quable_pim_scope:
  - full_access
webhook_events:
  - document.create
  - document.update
```

Available webhook events include:

- `document.create` — Triggered when a new document is created.
- `document.update` — Triggered when an existing document is updated.

**Note:** A maximum of 5 events can be registered per webhook.

---

#### Register webhooks during installation

Update the install route (`src/app/install/route.ts`) to automatically register webhooks when the app is installed:

```ts
import prisma from "@/lib/prisma";
import { QuablePimClient } from "@quable/quable-pim-js";
import { NextResponse } from "next/server";
import { readFile } from "fs/promises";
import * as yaml from "js-yaml";
import { join } from "path";

interface QuableAppConfig {
  application_type: string;
  quable_pim_scope: string[];
  webhook_events?: string[];
}

async function getAppConfig(): Promise<QuableAppConfig> {
  const filePath = join(process.cwd(), "quable.app.yml");
  const yamlContent = await readFile(filePath, "utf8");
  return yaml.load(yamlContent) as QuableAppConfig;
}

async function registerWebhooks(
  pimClient: QuablePimClient,
  events: string[]
): Promise<void> {
  const url = `${process.env.NEXT_PUBLIC_APP_URL}/webhook`;
  const name = "Quable App Template - Webhook";

  const payload = {
    active: true,
    name,
    url,
    events,
  };

  const existingWebhooks = await pimClient.API.REST.Webhook.getAll({
    url,
    name,
  });

  if (existingWebhooks?.length > 0) {
    await pimClient.API.REST.Webhook.update(existingWebhooks[0].id, payload);
  } else {
    await pimClient.API.REST.Webhook.create(payload);
  }
}

export async function POST(request: Request): Promise<NextResponse> {
  try {
    const requestPayload = await request.json();
    const { quableInstanceName, quableAuthToken } = requestPayload.data;

    const pimClient = new QuablePimClient({
      apiToken: quableAuthToken,
      instanceName: quableInstanceName,
    });

    // Validate the token
    await pimClient.API.REST.User.getAll({ limit: 1, type: "api" });

    // Store or update the instance
    await prisma.quableInstance.upsert({
      where: { name: quableInstanceName },
      create: { name: quableInstanceName, token: quableAuthToken },
      update: { name: quableInstanceName, token: quableAuthToken },
    });

    // Register webhooks from quable.app.yml
    const appConfig = await getAppConfig();
    if (appConfig.webhook_events?.length) {
      await registerWebhooks(pimClient, appConfig.webhook_events);
    }

    return NextResponse.json(
      { message: `QuableApp installed on ${quableInstanceName}.quable.com` },
      { status: 200 }
    );
  } catch (error) {
    console.error("Installation error:", error);
    return NextResponse.json(
      { message: "Installation failed. Verify your credentials and try again." },
      { status: 500 }
    );
  }
}
```

The `registerWebhooks` function:
1. Builds the webhook URL pointing to your app's `/webhook` endpoint.
2. Checks if a webhook with the same URL and name already exists.
3. Updates the existing webhook or creates a new one.

---

### 2. Listen to webhook events

Create an API route to receive webhook events. Create the file `src/app/webhook/route.ts`:

```ts
import { NextResponse } from "next/server";

export async function POST(request: Request): Promise<NextResponse> {
  try {
    const payload = await request.json();

    console.log("Webhook received:", payload);

    // Process the event based on its type
    switch (payload.code) {
      case "document.create":
        await handleDocumentCreate(payload);
        break;
      case "document.update":
        await handleDocumentUpdate(payload);
        break;
      default:
        console.log("Unhandled webhook event:", payload.code);
    }

    return NextResponse.json({}, { status: 200 });
  } catch (error) {
    console.error("Webhook processing error:", error);
    return NextResponse.json(
      { error: "Webhook processing failed" },
      { status: 500 }
    );
  }
}

async function handleDocumentCreate(payload: WebhookPayload) {
  const { resource } = payload;
  console.log(`Document created: ${resource.code} (type: ${resource.document_type_code})`);
  // Add your business logic here
}

async function handleDocumentUpdate(payload: WebhookPayload) {
  const { resource } = payload;
  console.log(`Document updated: ${resource.code} (type: ${resource.document_type_code})`);
  // Add your business logic here
}

interface WebhookPayload {
  uuid: string;
  code: string;
  links: { rel: string; href: string }[];
  created_at: string;
  created_by: string;
  resource: {
    code: string;
    type: string;
    document_type_code: string;
    links: { rel: string; href: string }[];
    locales: {
      updated: string[];
      inherited: string[];
    };
    updated_items: string[];
  };
  process: {
    correlationId: string;
  };
}
```

#### Webhook payload format

When a webhook event is triggered, the PIM sends a POST request with the following payload structure:

```json
{
  "uuid": "0963114b-c3d9-4a5e-9a33-ccc493b73649",
  "code": "document.update",
  "links": [
    {
      "rel": "self",
      "href": "/api/events/0963114b-c3d9-4a5e-9a33-ccc492b73629"
    }
  ],
  "created_at": "2023-11-03T08:58:00+00:00",
  "created_by": "quable",
  "resource": {
    "code": "vans-sh-8-hi",
    "type": "document",
    "document_type_code": "product",
    "links": [{ "rel": "self", "href": "/api/documents/vans-sh-8-hi" }],
    "locales": {
      "updated": ["en_US"],
      "inherited": ["fr_FR"]
    },
    "updated_items": []
  },
  "process": {
    "correlationId": "bca5bc9d-6250-4049-8o22-7bfd2f9ce9f1"
  }
}
```

| Field | Description |
|-------|-------------|
| `uuid` | Unique event identifier |
| `code` | Event type (e.g., `document.create`, `document.update`) |
| `created_at` | Timestamp of the event |
| `created_by` | User or system that triggered the event |
| `resource.code` | Code of the affected resource (e.g., document code) |
| `resource.type` | Type of resource (`document`, etc.) |
| `resource.document_type_code` | Document type code (e.g., `product`) |
| `resource.locales.updated` | Locales that were modified |
| `process.correlationId` | ID linking related events |

---

### 3. HMAC signature verification

All requests sent from Quable PIM to your application can be authenticated using HMAC-SHA256. The PIM sends two custom headers:

- `X-Timestamp` — Unix timestamp of the request time.
- `X-Signature` — HMAC signature encoded in base64.

Add signature verification to your webhook route:

```ts
import { NextResponse } from "next/server";
import { createHmac, timingSafeEqual } from "crypto";

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || "";
const TIMESTAMP_TOLERANCE = 300; // 5 minutes in seconds

function verifySignature(
  method: string,
  endpoint: string,
  timestamp: string,
  payload: string,
  receivedSignature: string
): boolean {
  // Check timestamp validity
  const currentTime = Math.floor(Date.now() / 1000);
  const timeDiff = Math.abs(currentTime - parseInt(timestamp, 10));
  if (timeDiff > TIMESTAMP_TOLERANCE) {
    return false;
  }

  // Build the string to sign: METHOD|ENDPOINT|TIMESTAMP|PAYLOAD
  const stringToSign = `${method.toUpperCase()}|${endpoint}|${timestamp}|${payload}`;

  // Calculate expected HMAC-SHA256
  const expectedSignature = createHmac("sha256", WEBHOOK_SECRET)
    .update(stringToSign)
    .digest("base64");

  // Constant-time comparison to prevent timing attacks
  try {
    return timingSafeEqual(
      Buffer.from(expectedSignature),
      Buffer.from(receivedSignature)
    );
  } catch {
    return false;
  }
}

export async function POST(request: Request): Promise<NextResponse> {
  const timestamp = request.headers.get("X-Timestamp") || "";
  const signature = request.headers.get("X-Signature") || "";
  const body = await request.text();

  // Verify HMAC signature
  const endpoint = `${process.env.NEXT_PUBLIC_APP_URL}/webhook`;
  if (!verifySignature("POST", endpoint, timestamp, body, signature)) {
    return NextResponse.json(
      { error: "Invalid signature" },
      { status: 401 }
    );
  }

  // Parse and process the verified payload
  const payload = JSON.parse(body);

  switch (payload.code) {
    case "document.create":
      await handleDocumentCreate(payload);
      break;
    case "document.update":
      await handleDocumentUpdate(payload);
      break;
    default:
      console.log("Unhandled webhook event:", payload.code);
  }

  return NextResponse.json({}, { status: 200 });
}
```

Add the webhook secret to your `.env` file:

```bash
WEBHOOK_SECRET="your-shared-secret-from-quable"
```

!!! Note
For more details on the HMAC authentication mechanism, see the [HMAC Security documentation](/quable-app/security/hmac).
!!!

---

#### Re-install to register webhooks

After updating your `quable.app.yml` with webhook events and deploying your changes, you need to **uninstall and reinstall** the app from your [partner portal dashboard](https://partners.quable.io/applications) to register the webhooks.

Once a document is created or updated in the PIM, your `/webhook` endpoint will receive the event payload.

!!!success
You now know how to register and handle Quable PIM webhook events in your Next.js application. You've learned how to define events, create API routes for receiving webhooks, and verify request authenticity with HMAC signatures.
!!!
