Raw
1 ---
2 title: "POST /webhooks/procurement — Approval Callback"
3 sidebarTitle: "POST /webhooks"
4 description: "Handle signed Procurement Express approval webhooks. Approved decisions resume sessions; rejected ones terminate them. Always verify the HMAC signature."
5 ---
6
7 ProcureNet sends a signed POST request to your webhook URL whenever a Procurement Express approval decision is made. Your handler receives a payload containing a `session_id`, a `decision` of either `approved` or `rejected`, the `actor` who made the decision, and an optional `reason`. Before acting on the payload, you must verify the `x-procure-signature` header using your shared webhook secret — requests that fail signature verification must be rejected to prevent spoofed approvals from influencing live procurement sessions. Return HTTP `204` on success; ProcureNet will retry on any non-`2xx` response.
8
9 ## Endpoint
10
11 ```http
12 POST /webhooks/procurement
13 ```
14
15 ## Authentication
16
17 ProcureNet signs each outbound webhook request with an HMAC-SHA256 signature derived from your shared secret and the raw request body. The signature arrives in the `x-procure-signature` header in the format:
18
19 ```text
20 x-procure-signature: sha256=<hex_digest>
21 ```
22
23 Your handler must recompute the expected signature and compare it to the header value **before** parsing or acting on the payload.
24
25 <Warning>
26 Always parse the request body using `express.raw()` (or equivalent raw body middleware) **before** performing signature verification. If you parse the body as JSON first, the serialization may differ from what ProcureNet signed, causing every verification to fail regardless of key validity.
27 </Warning>
28
29 ## Signature Verification
30
31 <Steps>
32 <Step title="Receive the raw request body">
33 Configure your route to buffer the raw bytes of the request body. In Express, use `express.raw({ type: 'application/json' })` on the webhook route — do not apply `express.json()` globally before this route.
34 </Step>
35 <Step title="Read the x-procure-signature header">
36 Extract the `x-procure-signature` header from the incoming request. If the header is absent, reject the request immediately with `401`.
37 </Step>
38 <Step title="Recompute the expected signature">
39 Use HMAC-SHA256 with your `WEBHOOK_SECRET` and the raw request body buffer to compute the expected digest. Prepend `sha256=` to match the header format.
40 </Step>
41 <Step title="Compare signatures">
42 Compare the computed signature to the header value. Use a constant-time comparison where possible to avoid timing attacks. Reject with `401` on mismatch.
43 </Step>
44 <Step title="Parse and handle the payload">
45 Parse the raw body as JSON only after verification passes. Act on the `decision` field and return `204`.
46 </Step>
47 </Steps>
48
49 ## Code Example
50
51 <CodeGroup>
52 ```typescript Express (TypeScript)
53 import express from 'express';
54 import { createHmac } from 'crypto';
55
56 const app = express();
57
58 app.post(
59 '/webhooks/procurement',
60 express.raw({ type: 'application/json' }),
61 (req, res) => {
62 const signature = req.headers['x-procure-signature'] as string;
63
64 if (!signature) {
65 return res.status(401).send('Missing signature');
66 }
67
68 const expected =
69 'sha256=' +
70 createHmac('sha256', process.env.WEBHOOK_SECRET!)
71 .update(req.body)
72 .digest('hex');
73
74 if (signature !== expected) {
75 return res.status(401).send('Invalid signature');
76 }
77
78 const payload = JSON.parse(req.body.toString()) as {
79 session_id: string;
80 decision: 'approved' | 'rejected';
81 actor: string;
82 reason?: string;
83 };
84
85 if (payload.decision === 'approved') {
86 // Resume the suspended orchestration flow for this session
87 console.log(`Session ${payload.session_id} approved by ${payload.actor}`);
88 } else {
89 // Terminate the session
90 console.log(`Session ${payload.session_id} rejected: ${payload.reason ?? 'no reason given'}`);
91 }
92
93 return res.status(204).send();
94 }
95 );
96 ```
97
98 ```typescript Next.js API Route
99 import { createHmac } from 'crypto';
100 import type { NextApiRequest, NextApiResponse } from 'next';
101
102 export const config = {
103 api: { bodyParser: false },
104 };
105
106 async function getRawBody(req: NextApiRequest): Promise<Buffer> {
107 return new Promise((resolve, reject) => {
108 const chunks: Buffer[] = [];
109 req.on('data', chunk => chunks.push(chunk));
110 req.on('end', () => resolve(Buffer.concat(chunks)));
111 req.on('error', reject);
112 });
113 }
114
115 export default async function handler(req: NextApiRequest, res: NextApiResponse) {
116 if (req.method !== 'POST') {
117 return res.status(405).end();
118 }
119
120 const rawBody = await getRawBody(req);
121 const signature = req.headers['x-procure-signature'] as string;
122 const expected =
123 'sha256=' +
124 createHmac('sha256', process.env.WEBHOOK_SECRET!)
125 .update(rawBody)
126 .digest('hex');
127
128 if (signature !== expected) {
129 return res.status(401).json({ error: 'Invalid signature' });
130 }
131
132 const payload = JSON.parse(rawBody.toString());
133
134 // Handle approved / rejected
135 if (payload.decision === 'approved') {
136 await resumeSession(payload.session_id);
137 } else {
138 await terminateSession(payload.session_id, payload.reason);
139 }
140
141 return res.status(204).end();
142 }
143 ```
144 </CodeGroup>
145
146 ## Payload Fields
147
148 <ParamField body="session_id" type="string" required>
149 The ProcureNet session ID that this approval decision applies to. Use this to look up the suspended session in your orchestration state.
150 </ParamField>
151
152 <ParamField body="decision" type="string" required>
153 The approval outcome. One of:
154 - `"approved"` — the session should be resumed and the procurement flow continued.
155 - `"rejected"` — the session should be terminated and any reserved resources released.
156 </ParamField>
157
158 <ParamField body="actor" type="string" required>
159 The identity of the person or system that made the approval decision. This value is provided by Procurement Express and may be an email address, username, or system identifier depending on your Procurement Express configuration.
160 </ParamField>
161
162 <ParamField body="reason" type="string">
163 An optional human-readable explanation for the decision. This is most commonly populated for `rejected` decisions to explain why a session was not approved. Log this value for auditing purposes.
164 </ParamField>
165
166 ### Example Payload — Approved
167
168 ```json
169 {
170 "session_id": "sess_a1b2c3d4",
171 "decision": "approved",
172 "actor": "procurement-manager@example.com",
173 "reason": "Budget confirmed for Q3 vendor onboarding"
174 }
175 ```
176
177 ### Example Payload — Rejected
178
179 ```json
180 {
181 "session_id": "sess_e5f6g7h8",
182 "decision": "rejected",
183 "actor": "compliance-bot",
184 "reason": "Vendor not on approved supplier list"
185 }
186 ```
187
188 ## Response
189
190 Return HTTP `204 No Content` to acknowledge the webhook. Do not return a body.
191
192 | Status | Meaning |
193 |---|---|
194 | `204` | Webhook received and processed successfully |
195 | `401` | Signature verification failed — request rejected |
196
197 <Note>
198 ProcureNet retries webhook delivery on any non-`2xx` response using an exponential back-off policy. Make your handler idempotent — the same `session_id` + `decision` combination may arrive more than once. Check whether the session is already in its target state before applying side effects.
199 </Note>
200
201 ## Decision Handling
202
203 <CardGroup cols={2}>
204 <Card title="approved" icon="circle-check">
205 Resume the suspended orchestration flow. The session re-enters the state machine at the point it was paused. You may want to call `POST /api/v7/sessions/{id}/transition` to advance to the next state after resuming.
206 </Card>
207 <Card title="rejected" icon="circle-xmark">
208 Terminate the session immediately. Release any held inventory or funding reservations, notify the buyer, and close the session. Log the `actor` and `reason` fields for your audit trail.
209 </Card>
210 </CardGroup>
211
212 ## Security Checklist
213
214 <Accordion title="Webhook security best practices">
215 - **Never skip signature verification.** Even in development, always verify the `x-procure-signature` header.
216 - **Use a raw body parser.** Applying a JSON parser before verification will invalidate the HMAC check.
217 - **Store `WEBHOOK_SECRET` securely.** Treat this secret like a private key — use environment variables or a secrets manager, never hardcode it.
218 - **Make handlers idempotent.** ProcureNet may deliver the same event more than once. Guard against double-processing by checking current session state before applying transitions.
219 - **Respond promptly.** Complete signature verification and enqueue any heavy work asynchronously. Return `204` within a few seconds to avoid triggering ProcureNet's retry logic.
220 - **Log all decisions.** Store the full payload, `actor`, and timestamp for each webhook received. Approval decisions are auditable events.
221 </Accordion>