Skip to main content

Custom webhook consumer

If you want maximum control, the best ProcureIQ integration pattern is usually a custom webhook consumer. ProcureIQ sends you real-time event notifications, and your service decides what to do with them.

This guide includes:

  • a complete Node.js + Express example
  • a complete Python + FastAPI example
  • example payloads for every ProcureIQ event type
  • practical advice for retries, idempotency, and performance

How ProcureIQ webhooks work

When an event happens in ProcureIQ, for example an order is created or a payment succeeds, ProcureIQ sends an HTTP POST request to your webhook endpoint.

Each webhook request includes:

  • a JSON payload
  • an event type like order.created
  • a unique event ID
  • a timestamp
  • an HMAC signature in x-procureiq-signature

Your job is to:

  1. verify the signature
  2. acknowledge the request quickly
  3. process the event in the background
  4. make your event processing idempotent so duplicate deliveries do not break your system

ProcureIQ webhook headers

Your endpoint will typically receive headers like:

  • content-type: application/json
  • x-procureiq-signature: sha256=...
  • x-procureiq-event: order.created
  • x-procureiq-delivery: <delivery-id>
  • x-procureiq-timestamp: <unix-timestamp>

Webhook payload format

All ProcureIQ outbound webhook payloads follow the same envelope:

{
"id": "evt_01HX8P8Z8G6D0A0A2M4Q7X5Z8T",
"type": "order.created",
"version": "1.0",
"created": 1711234567,
"data": {
"orderId": "ord_01HX8P4XB3E5N4A4YCM9YQWJJA",
"userId": "usr_01HX8P2PVTQ3M2K1K0EXAMPLE",
"totalAmount": 742.5,
"currency": "USD",
"itemCount": 3
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

Node.js + Express example

This example is complete and ready to copy, paste, and adapt.

const express = require("express");
const crypto = require("crypto");

const app = express();
const processedEvents = new Set();

app.use(express.raw({ type: "application/json" }));

app.post("/webhooks/procureiq", (req, res) => {
const signature = req.headers["x-procureiq-signature"];

const expected =
"sha256=" +
crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(req.body)
.digest("hex");

if (!signature || signature !== expected) {
return res.status(401).send("Unauthorized");
}

const event = JSON.parse(req.body.toString("utf8"));

// Always acknowledge immediately so ProcureIQ does not wait on your business logic.
res.sendStatus(200);

// Handle event asynchronously after acknowledging receipt.
handleEvent(event).catch((error) => {
console.error("Failed to process ProcureIQ event", {
eventId: event.id,
eventType: event.type,
error,
});
});
});

async function handleEvent(event) {
// Basic idempotency protection.
if (processedEvents.has(event.id)) {
console.log("Skipping duplicate ProcureIQ event", event.id);
return;
}

processedEvents.add(event.id);

switch (event.type) {
case "order.created":
console.log("New order received", event.data.orderId);
break;

case "payment.succeeded":
console.log("Payment confirmed", event.data.paymentId);
break;

case "support.ticket_created":
console.log("Support ticket created", event.data.ticketId);
break;

default:
console.log("Unhandled ProcureIQ event", event.type);
}
}

const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`ProcureIQ webhook consumer listening on port ${port}`);
});

Why this Express example is structured this way

  • express.raw() is used so the HMAC is computed from the exact raw request body
  • the response is sent immediately before business logic runs
  • duplicate event IDs are ignored
  • the event type is routed in one place for clarity

Python + FastAPI example

This example uses FastAPI, HMAC verification, and BackgroundTasks to acknowledge immediately and process later.

import hashlib
import hmac
import json
import os
from typing import Any, Dict, Set

from fastapi import BackgroundTasks, FastAPI, Header, HTTPException, Request

app = FastAPI()
processed_events: Set[str] = set()


async def process_event(event: Dict[str, Any]) -> None:
event_id = event["id"]
event_type = event["type"]

if event_id in processed_events:
print(f"Skipping duplicate ProcureIQ event {event_id}")
return

processed_events.add(event_id)

if event_type == "order.created":
print("New order received", event["data"]["orderId"])
elif event_type == "payment.succeeded":
print("Payment confirmed", event["data"]["paymentId"])
elif event_type == "support.ticket_created":
print("Support ticket created", event["data"]["ticketId"])
else:
print("Unhandled ProcureIQ event", event_type)


@app.post("/webhooks/procureiq")
async def procureiq_webhook(
request: Request,
background_tasks: BackgroundTasks,
x_procureiq_signature: str | None = Header(default=None),
) -> Dict[str, str]:
secret = os.environ.get("WEBHOOK_SECRET")
if not secret:
raise HTTPException(status_code=500, detail="Missing WEBHOOK_SECRET")

raw_body = await request.body()

expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()

if not x_procureiq_signature or not hmac.compare_digest(
x_procureiq_signature, expected
):
raise HTTPException(status_code=401, detail="Unauthorized")

event = json.loads(raw_body.decode("utf-8"))

# Acknowledge immediately. FastAPI returns this response while the
# background task processes the event.
background_tasks.add_task(process_event, event)
return {"status": "ok"}

Why this FastAPI example is structured this way

  • request.body() gives access to the raw bytes needed for signature verification
  • hmac.compare_digest() is safer than a simple string comparison
  • BackgroundTasks lets you return immediately and process after the response

Payload examples for every event type

These examples use realistic-looking values. All payloads follow the same envelope and only the type and data sections change.

Search events

search.started

{
"id": "evt_01HX900000000000000000001",
"type": "search.started",
"version": "1.0",
"created": 1711234501,
"data": {
"searchResultId": "sr_01HX8ZK3W2WZ8F8A2RJD3QZZ01",
"briefId": "brief_01HX8ZJQ6XEZ7BQ4K7EW5QX991",
"userId": "usr_01HX8ZHQ7E8N6G3M2A1P0AB123",
"productType": "industrial-fan"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

search.completed

{
"id": "evt_01HX900000000000000000002",
"type": "search.completed",
"version": "1.0",
"created": 1711234522,
"data": {
"searchResultId": "sr_01HX8ZK3W2WZ8F8A2RJD3QZZ01",
"totalFound": 42,
"durationMs": 18230
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

search.failed

{
"id": "evt_01HX900000000000000000003",
"type": "search.failed",
"version": "1.0",
"created": 1711234528,
"data": {
"searchResultId": "sr_01HX8ZK3W2WZ8F8A2RJD3QZZ01",
"error": "Crawler timeout after 30 seconds"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}
Order events

order.created

{
"id": "evt_01HX900000000000000000010",
"type": "order.created",
"version": "1.0",
"created": 1711234601,
"data": {
"orderId": "ord_01HX900A8XG6PA6MT9G8QYB101",
"userId": "usr_01HX8ZHQ7E8N6G3M2A1P0AB123",
"totalAmount": 742.5,
"currency": "USD",
"itemCount": 3
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

order.confirmed

{
"id": "evt_01HX900000000000000000011",
"type": "order.confirmed",
"version": "1.0",
"created": 1711234620,
"data": {
"orderId": "ord_01HX900A8XG6PA6MT9G8QYB101",
"paymentId": "pay_01HX900QHJ0S1T8X6M5N4P3R22"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

order.shipped

{
"id": "evt_01HX900000000000000000012",
"type": "order.shipped",
"version": "1.0",
"created": 1711238601,
"data": {
"orderId": "ord_01HX900A8XG6PA6MT9G8QYB101",
"trackingNumber": "DHL-7845123099",
"carrierId": "dhl",
"estimatedDelivery": "2026-05-03"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

order.delivered

{
"id": "evt_01HX900000000000000000013",
"type": "order.delivered",
"version": "1.0",
"created": 1711321001,
"data": {
"orderId": "ord_01HX900A8XG6PA6MT9G8QYB101",
"deliveredAt": "2026-05-03T14:22:19.000Z"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

order.cancelled

{
"id": "evt_01HX900000000000000000014",
"type": "order.cancelled",
"version": "1.0",
"created": 1711234705,
"data": {
"orderId": "ord_01HX900A8XG6PA6MT9G8QYB101",
"reason": "Customer requested cancellation before dispatch"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}
Payment events

payment.initiated

{
"id": "evt_01HX900000000000000000020",
"type": "payment.initiated",
"version": "1.0",
"created": 1711234610,
"data": {
"paymentId": "pay_01HX900QHJ0S1T8X6M5N4P3R22",
"orderId": "ord_01HX900A8XG6PA6MT9G8QYB101",
"provider": "stripe",
"amount": 742.5,
"currency": "USD"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

payment.succeeded

{
"id": "evt_01HX900000000000000000021",
"type": "payment.succeeded",
"version": "1.0",
"created": 1711234623,
"data": {
"paymentId": "pay_01HX900QHJ0S1T8X6M5N4P3R22",
"orderId": "ord_01HX900A8XG6PA6MT9G8QYB101",
"paidAt": "2026-04-28T09:17:03.000Z"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

payment.failed

{
"id": "evt_01HX900000000000000000022",
"type": "payment.failed",
"version": "1.0",
"created": 1711234629,
"data": {
"paymentId": "pay_01HX900QHJ0S1T8X6M5N4P3R22",
"orderId": "ord_01HX900A8XG6PA6MT9G8QYB101",
"reason": "Card authentication failed"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

payment.refunded

{
"id": "evt_01HX900000000000000000023",
"type": "payment.refunded",
"version": "1.0",
"created": 1711240000,
"data": {
"paymentId": "pay_01HX900QHJ0S1T8X6M5N4P3R22",
"orderId": "ord_01HX900A8XG6PA6MT9G8QYB101",
"amount": 742.5
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}
Product events

product.listed

{
"id": "evt_01HX900000000000000000030",
"type": "product.listed",
"version": "1.0",
"created": 1711236000,
"data": {
"productListingId": "prod_01HX90VQ31M7T4F5G6H7J8K901",
"category": "INDUSTRIAL_EQUIPMENT",
"displayPrice": 168
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

product.approved

{
"id": "evt_01HX900000000000000000031",
"type": "product.approved",
"version": "1.0",
"created": 1711236100,
"data": {
"merchantProductId": "mp_01HX90TRMSYV0W1X2Y3Z4A5B67",
"productListingId": "prod_01HX90VQ31M7T4F5G6H7J8K901"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

product.rejected

{
"id": "evt_01HX900000000000000000032",
"type": "product.rejected",
"version": "1.0",
"created": 1711236150,
"data": {
"merchantProductId": "mp_01HX90TRMSYV0W1X2Y3Z4A5B67",
"reason": "Submitted images did not meet marketplace quality guidelines"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}
User events

user.registered

{
"id": "evt_01HX900000000000000000040",
"type": "user.registered",
"version": "1.0",
"created": 1711233000,
"data": {
"userId": "usr_01HX8ZHQ7E8N6G3M2A1P0AB123",
"email": "amina.bello@northstar-imports.com",
"role": "BUYER"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

user.tier_changed

{
"id": "evt_01HX900000000000000000041",
"type": "user.tier_changed",
"version": "1.0",
"created": 1711233200,
"data": {
"userId": "usr_01HX8ZHQ7E8N6G3M2A1P0AB123",
"oldTier": "FREE",
"newTier": "GOLD"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}
Support events

support.ticket_created

{
"id": "evt_01HX900000000000000000050",
"type": "support.ticket_created",
"version": "1.0",
"created": 1711237000,
"data": {
"ticketId": "tkt_01HX9131F6W03M4M5N6P7Q8R99",
"category": "PAYMENT",
"priority": "HIGH",
"userId": "usr_01HX8ZHQ7E8N6G3M2A1P0AB123"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

support.ticket_resolved

{
"id": "evt_01HX900000000000000000051",
"type": "support.ticket_resolved",
"version": "1.0",
"created": 1711240001,
"data": {
"ticketId": "tkt_01HX9131F6W03M4M5N6P7Q8R99",
"resolvedBy": "HUMAN"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

support.csat_submitted

{
"id": "evt_01HX900000000000000000052",
"type": "support.csat_submitted",
"version": "1.0",
"created": 1711240900,
"data": {
"ticketId": "tkt_01HX9131F6W03M4M5N6P7Q8R99",
"rating": 5
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}
Advertising events

ad.campaign_started

{
"id": "evt_01HX900000000000000000060",
"type": "ad.campaign_started",
"version": "1.0",
"created": 1711238000,
"data": {
"campaignId": "adcmp_01HX91V7XJ98K7M6N5P4Q3R210",
"advertiserId": "adv_01HX91TDKRT5F5D4C3B2A1908Z"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

ad.budget_exhausted

{
"id": "evt_01HX900000000000000000061",
"type": "ad.budget_exhausted",
"version": "1.0",
"created": 1711290000,
"data": {
"campaignId": "adcmp_01HX91V7XJ98K7M6N5P4Q3R210"
},
"metadata": {
"procureiqVersion": "1.0.0",
"environment": "production"
}
}

Error handling best practices

When you build a ProcureIQ webhook consumer, these practices matter more than almost anything else.

1. Always return 200 immediately

ProcureIQ expects your endpoint to acknowledge the webhook quickly.

Do this:

  • validate the signature
  • enqueue or background the work
  • return success immediately

Avoid doing this inside the request thread:

  • long database writes
  • external CRM API calls
  • large file processing
  • slow analytics jobs

2. Process events asynchronously

Treat webhooks as event notifications, not full workflows by themselves.

Good patterns:

  • queue the event in Redis, BullMQ, SQS, RabbitMQ, or a database table
  • let a worker process it afterward
  • store processing results separately from webhook receipt

3. Implement idempotency

Webhook providers retry. That is normal.

Use the ProcureIQ event ID as an idempotency key:

  • store processed event IDs in a database table
  • skip processing if the same event ID appears again

4. Retry your own downstream processing

Even if your webhook endpoint responds with 200, your own systems may still fail later.

Use exponential backoff, for example:

  • retry after 30 seconds
  • retry after 2 minutes
  • retry after 10 minutes
  • move to a dead-letter queue if repeated failures continue

5. Keep endpoint response time below 30 seconds

ProcureIQ will time out slow webhook deliveries. In practice, you should aim much lower than that.

A good target is:

  • under 1 second for the HTTP acknowledgement

Additional production recommendations

  • log event.id, event.type, and your internal processing result
  • alert on signature failures and repeated processing failures
  • separate sandbox and production webhook endpoints
  • rotate webhook secrets periodically
  • build replay tooling so you can reprocess events safely

Next steps

Once your custom consumer is live, you can:

  • route ProcureIQ events into internal systems
  • build customer notifications and fulfillment automations
  • create finance and support workflows
  • enrich your own reporting pipeline with real-time procurement data

If you want a faster low-code starting point, see Zapier or n8n.