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:
- verify the signature
- acknowledge the request quickly
- process the event in the background
- 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/jsonx-procureiq-signature: sha256=...x-procureiq-event: order.createdx-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 verificationhmac.compare_digest()is safer than a simple string comparisonBackgroundTaskslets 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.