|
1
|
--- |
|
2
|
title: "How to Authenticate Every Request to ProcureNet APIs" |
|
3
|
sidebarTitle: "Authentication" |
|
4
|
description: "ProcureNet uses four credential types — mod keys, JWT tokens, webhook HMAC signatures, and admin keys — each scoped to a specific part of the API." |
|
5
|
--- |
|
6
|
|
|
7
|
ProcureNet authenticates requests through four distinct mechanisms, each scoped to a different surface of the API. Wallet and payment endpoints use a mod key passed as a custom header. Session endpoints require a JWT bearer token. Inbound webhooks from Procurement Express carry an HMAC signature you must verify before processing. Admin override endpoints require a separate admin key. Understanding which credential applies where prevents auth failures and keeps your integration secure. |
|
8
|
|
|
9
|
--- |
|
10
|
|
|
11
|
## API Keys (Wallet and Payment Endpoints) |
|
12
|
|
|
13
|
Wallet and payment endpoints authenticate with a **mod key** passed in the `x-perplexity-mod-key` header. You receive this key from the ProcureNet dashboard when you create a workspace. Use it on all requests to the USDC payment bridge: |
|
14
|
|
|
15
|
- `POST /mods/procurement_wallet/usdc/request` — create a USDC payment request |
|
16
|
- `GET /mods/procurement_wallet/usdc/watch/{paymentId}` — poll for payment confirmation |
|
17
|
|
|
18
|
```typescript |
|
19
|
const response = await fetch( |
|
20
|
`${process.env.PROCURENET_BASE_URL}/mods/procurement_wallet/usdc/request`, |
|
21
|
{ |
|
22
|
method: "POST", |
|
23
|
headers: { |
|
24
|
"Content-Type": "application/json", |
|
25
|
"x-perplexity-mod-key": process.env.PROCURENET_MOD_KEY!, |
|
26
|
}, |
|
27
|
body: JSON.stringify({ |
|
28
|
tenant_id: "your-tenant-id", |
|
29
|
amount_usdc: 250, |
|
30
|
network: "base", |
|
31
|
token: "USDC", |
|
32
|
memo: "Evaluation payout — Assignment #A-1042", |
|
33
|
metadata: { |
|
34
|
evaluator_id: "eval_0192XZ", |
|
35
|
assignment_id: "A-1042", |
|
36
|
}, |
|
37
|
}), |
|
38
|
} |
|
39
|
); |
|
40
|
|
|
41
|
const payment = await response.json(); |
|
42
|
// { payment_id, deposit_address, qr_code, expires_at } |
|
43
|
``` |
|
44
|
|
|
45
|
<Note> |
|
46
|
Your mod key carries full access to the wallet API. Never embed it in client-side code, browser bundles, or public repositories. Always load it from a server-side environment variable. |
|
47
|
</Note> |
|
48
|
|
|
49
|
--- |
|
50
|
|
|
51
|
## JWT Bearer Tokens (Session Endpoints) |
|
52
|
|
|
53
|
Session endpoints require a **JWT bearer token** in the standard `Authorization` header. You receive a JWT after authenticating with ProcureNet's identity service (consult your onboarding docs for the token exchange flow specific to your setup). Pass this token on every request to the following endpoints: |
|
54
|
|
|
55
|
| Endpoint | Method | Purpose | |
|
56
|
|---|---|---| |
|
57
|
| `/api/v7/sessions/{id}/prefill` | GET | Retrieve prefill data for resolved sessions | |
|
58
|
| `/api/v7/sessions/{id}/transition` | POST | Advance the session state machine | |
|
59
|
| `/api/v7/sessions/{id}/escrow-accept` | POST | Accept escrow for high-value transactions | |
|
60
|
| `/api/v7/sessions/{id}/funding-method` | POST | Set the payment method on a session | |
|
61
|
|
|
62
|
```typescript |
|
63
|
const sessionId = "sess_01JKAB3MXPQ7RVTZWN5"; |
|
64
|
|
|
65
|
// Retrieve prefill data (resolved sessions only — WCS ≥ 100) |
|
66
|
const prefillResponse = await fetch( |
|
67
|
`${process.env.PROCURENET_BASE_URL}/api/v7/sessions/${sessionId}/prefill`, |
|
68
|
{ |
|
69
|
method: "GET", |
|
70
|
headers: { |
|
71
|
Authorization: `Bearer ${process.env.PROCURENET_JWT}`, |
|
72
|
}, |
|
73
|
} |
|
74
|
); |
|
75
|
|
|
76
|
const prefill = await prefillResponse.json(); |
|
77
|
// { customer: { ... }, payment_preferences: [...] } |
|
78
|
``` |
|
79
|
|
|
80
|
```typescript |
|
81
|
// Set a funding method on the session |
|
82
|
const fundingResponse = await fetch( |
|
83
|
`${process.env.PROCURENET_BASE_URL}/api/v7/sessions/${sessionId}/funding-method`, |
|
84
|
{ |
|
85
|
method: "POST", |
|
86
|
headers: { |
|
87
|
"Content-Type": "application/json", |
|
88
|
Authorization: `Bearer ${process.env.PROCURENET_JWT}`, |
|
89
|
}, |
|
90
|
body: JSON.stringify({ |
|
91
|
method: "card", |
|
92
|
token: "pm_card_visa_deferred", |
|
93
|
}), |
|
94
|
} |
|
95
|
); |
|
96
|
``` |
|
97
|
|
|
98
|
<Tip> |
|
99
|
JWTs are scoped to the session they were issued for. Passing a token from session A on a request for session B returns a `403 Forbidden` response. |
|
100
|
</Tip> |
|
101
|
|
|
102
|
--- |
|
103
|
|
|
104
|
## Webhook Signature Verification |
|
105
|
|
|
106
|
When Procurement Express sends an approval callback to your `POST /webhooks/procurement` endpoint, it includes an HMAC signature in the `x-procure-signature` header. You **must** verify this signature before processing the payload — unverified webhooks can be spoofed by any party that knows your endpoint URL. |
|
107
|
|
|
108
|
Verification works by computing an HMAC-SHA256 digest of the raw request body using your webhook secret, then comparing it to the value in `x-procure-signature`. Use a constant-time comparison to avoid timing attacks. |
|
109
|
|
|
110
|
```typescript |
|
111
|
import { createHmac, timingSafeEqual } from "crypto"; |
|
112
|
import type { IncomingMessage, ServerResponse } from "http"; |
|
113
|
|
|
114
|
function verifyProcureSignature( |
|
115
|
rawBody: Buffer, |
|
116
|
signatureHeader: string, |
|
117
|
secret: string |
|
118
|
): boolean { |
|
119
|
const expected = createHmac("sha256", secret) |
|
120
|
.update(rawBody) |
|
121
|
.digest("hex"); |
|
122
|
|
|
123
|
const expectedBuf = Buffer.from(expected, "utf8"); |
|
124
|
const receivedBuf = Buffer.from(signatureHeader, "utf8"); |
|
125
|
|
|
126
|
// Buffers must be the same length before timingSafeEqual |
|
127
|
if (expectedBuf.length !== receivedBuf.length) { |
|
128
|
return false; |
|
129
|
} |
|
130
|
|
|
131
|
return timingSafeEqual(expectedBuf, receivedBuf); |
|
132
|
} |
|
133
|
|
|
134
|
// Example Express handler |
|
135
|
app.post( |
|
136
|
"/webhooks/procurement", |
|
137
|
express.raw({ type: "application/json" }), |
|
138
|
(req: IncomingMessage & { body: Buffer }, res: ServerResponse) => { |
|
139
|
const signature = req.headers["x-procure-signature"] as string; |
|
140
|
|
|
141
|
if (!signature) { |
|
142
|
res.writeHead(400); |
|
143
|
res.end("Missing signature"); |
|
144
|
return; |
|
145
|
} |
|
146
|
|
|
147
|
const isValid = verifyProcureSignature( |
|
148
|
req.body, |
|
149
|
signature, |
|
150
|
process.env.PROCURENET_WEBHOOK_SECRET! |
|
151
|
); |
|
152
|
|
|
153
|
if (!isValid) { |
|
154
|
res.writeHead(401); |
|
155
|
res.end("Invalid signature"); |
|
156
|
return; |
|
157
|
} |
|
158
|
|
|
159
|
const payload = JSON.parse(req.body.toString()); |
|
160
|
// payload: { session_id, decision: "approved" | "rejected", actor, reason? } |
|
161
|
console.log("Procurement approval received:", payload); |
|
162
|
|
|
163
|
res.writeHead(204); |
|
164
|
res.end(); |
|
165
|
} |
|
166
|
); |
|
167
|
``` |
|
168
|
|
|
169
|
<Warning> |
|
170
|
Always read the raw request body **before** any JSON parsing middleware runs. If your framework parses the body into an object first, re-serializing it can change whitespace and break the HMAC digest, causing all verification checks to fail. |
|
171
|
</Warning> |
|
172
|
|
|
173
|
--- |
|
174
|
|
|
175
|
## Admin Keys |
|
176
|
|
|
177
|
The `x-admin-key` header is required for manual payment confirmation at `POST /mods/procurement_wallet/usdc/manual-confirm`. This endpoint exists for operational overrides — for example, when an on-chain confirmation is delayed and you need to unblock a field evaluator's payout manually. |
|
178
|
|
|
179
|
Pass both your mod key and your admin key together on this request: |
|
180
|
|
|
181
|
```typescript |
|
182
|
const confirmResponse = await fetch( |
|
183
|
`${process.env.PROCURENET_BASE_URL}/mods/procurement_wallet/usdc/manual-confirm`, |
|
184
|
{ |
|
185
|
method: "POST", |
|
186
|
headers: { |
|
187
|
"Content-Type": "application/json", |
|
188
|
"x-perplexity-mod-key": process.env.PROCURENET_MOD_KEY!, |
|
189
|
"x-admin-key": process.env.PROCURENET_ADMIN_KEY!, |
|
190
|
}, |
|
191
|
body: JSON.stringify({ |
|
192
|
payment_id: "pay_01HXYZ77BBQRST", |
|
193
|
}), |
|
194
|
} |
|
195
|
); |
|
196
|
|
|
197
|
const result = await confirmResponse.json(); |
|
198
|
// { confirmed: true, transaction_hash: "0xabc..." } |
|
199
|
``` |
|
200
|
|
|
201
|
<Warning> |
|
202
|
Admin keys grant override authority over payment confirmation. Treat them with the same security rigor as private keys — restrict access to server-side infrastructure only, rotate them immediately if exposed, and audit their use through your logging pipeline. |
|
203
|
</Warning> |
|
204
|
|
|
205
|
--- |
|
206
|
|
|
207
|
## Error Reference |
|
208
|
|
|
209
|
When authentication fails, ProcureNet returns one of the following HTTP status codes: |
|
210
|
|
|
211
|
| Status | Meaning | Common Causes | |
|
212
|
|---|---|---| |
|
213
|
| `401 Unauthorized` | Missing or invalid credential | No `Authorization` header, expired JWT, malformed mod key, failed HMAC verification | |
|
214
|
| `403 Forbidden` | Valid credential but insufficient access | JWT scoped to a different session, prefill requested on a non-resolved session (WCS < 100), admin endpoint called without `x-admin-key` | |
|
215
|
| `429 Too Many Requests` | Rate limit exceeded | Too many requests in a short window; back off and retry with exponential delay | |
|
216
|
|
|
217
|
<Accordion title="Handling 401 vs 403 in your client"> |
|
218
|
A `401` means the credential itself is unrecognized or malformed — check that you're sending the right header name, that the token hasn't expired, and that you haven't accidentally URL-encoded the value. |
|
219
|
|
|
220
|
A `403` means ProcureNet recognized your credential but the specific action is not permitted. The most common cause is calling `GET /api/v7/sessions/{id}/prefill` on a session with a WCS score below 100. Check the `mode` field from your original orchestration response before attempting prefill. |
|
221
|
</Accordion> |
|
222
|
|
|
223
|
--- |
|
224
|
|
|
225
|
## Credential Summary |
|
226
|
|
|
227
|
<CardGroup cols={2}> |
|
228
|
<Card title="x-perplexity-mod-key" icon="key"> |
|
229
|
**Scope:** Wallet and USDC payment endpoints |
|
230
|
**Where to get it:** ProcureNet dashboard |
|
231
|
**Never expose:** client-side code or public repos |
|
232
|
</Card> |
|
233
|
<Card title="Authorization: Bearer JWT" icon="id-badge"> |
|
234
|
**Scope:** All `/api/v7/sessions/{id}/...` endpoints |
|
235
|
**Where to get it:** ProcureNet identity service token exchange |
|
236
|
**Scoped to:** individual sessions |
|
237
|
</Card> |
|
238
|
<Card title="x-procure-signature" icon="shield-check"> |
|
239
|
**Scope:** Inbound webhook verification |
|
240
|
**Where to get it:** ProcureNet dashboard (webhook secret) |
|
241
|
**Direction:** Inbound — you verify it, not send it |
|
242
|
</Card> |
|
243
|
<Card title="x-admin-key" icon="shield-halved"> |
|
244
|
**Scope:** Manual payment confirmation only |
|
245
|
**Where to get it:** ProcureNet account team |
|
246
|
**Never expose:** client-side code, logs, or public repos |
|
247
|
</Card> |
|
248
|
</CardGroup> |