| 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> |