#
Subscribe to Webhook Events
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 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:
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:
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:
- Builds the webhook URL pointing to your app's
/webhookendpoint. - Checks if a webhook with the same URL and name already exists.
- 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:
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:
{
"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"
}
}
#
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:
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:
WEBHOOK_SECRET="your-shared-secret-from-quable"
Note
For more details on the HMAC authentication mechanism, see the HMAC Security documentation.
#
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 to register the webhooks.
Once a document is created or updated in the PIM, your /webhook endpoint will receive the event payload.
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.