|
| 1 | +--- |
| 2 | +title: "Secure Payment Integration Guide" |
| 3 | +description: "Integrate a secure payment experience via a secured link (redirect)" |
| 4 | +--- |
| 5 | + |
| 6 | +## Overview |
| 7 | + |
| 8 | +This guide shows how to integrate Secure Payment Pages in a production-ready way. |
| 9 | + |
| 10 | +The recommended pattern is: |
| 11 | +1. Create secure payment links with `POST /v2/secure-payments` |
| 12 | +2. Store returned `requestIds` in your system |
| 13 | +3. Redirect the payer to the secure page |
| 14 | +4. Use webhooks as the source of truth for payment status updates |
| 15 | + |
| 16 | +## Prerequisites |
| 17 | + |
| 18 | +Before you integrate, make sure you have: |
| 19 | + |
| 20 | +- An API key or a Client ID linked to your integration domain |
| 21 | +- A webhook endpoint configured in Request Portal |
| 22 | +- Your webhook signing secret stored securely on your backend |
| 23 | + |
| 24 | +For setup details, see: |
| 25 | +- [Authentication](/api-reference/authentication) |
| 26 | +- [Webhooks](/api-reference/webhooks) |
| 27 | + |
| 28 | +## Quick start |
| 29 | + |
| 30 | +<Steps> |
| 31 | + <Step title="Create secure payment links"> |
| 32 | + Call `POST /v2/secure-payments` and store the returned `requestIds`. |
| 33 | + |
| 34 | + <RequestExample> |
| 35 | + ```bash cURL |
| 36 | + curl -X POST "https://api.request.network/v2/secure-payments" \ |
| 37 | + -H "x-api-key: YOUR_API_KEY" \ |
| 38 | + -H "Content-Type: application/json" \ |
| 39 | + -d '{ |
| 40 | + "requests": [ |
| 41 | + { |
| 42 | + "payee": "0x6923831ACf5c327260D7ac7C9DfF5b1c3cB3C7D7", |
| 43 | + "amount": "10", |
| 44 | + "invoiceCurrency": "USDC-base", |
| 45 | + "paymentCurrency": "USDC-base" |
| 46 | + } |
| 47 | + ] |
| 48 | + }' |
| 49 | + ``` |
| 50 | + </RequestExample> |
| 51 | + |
| 52 | + <ResponseExample> |
| 53 | + ```json 201 Created |
| 54 | + { |
| 55 | + "requestIds": [ |
| 56 | + "01e273ecc29d4b526df3a0f1f05ffc59372af8752c2b678096e49ac270416a7cdb" |
| 57 | + ], |
| 58 | + "securePaymentUrl": "https://secure.request.network/?token=01ABC123DEF456GHI789JKL", |
| 59 | + "token": "01ABC123DEF456GHI789JKL" |
| 60 | + } |
| 61 | + ``` |
| 62 | + </ResponseExample> |
| 63 | + </Step> |
| 64 | + |
| 65 | + <Step title="Persist payment mapping in your database"> |
| 66 | + Store at least: |
| 67 | + - your internal metadata |
| 68 | + - returned `requestIds` |
| 69 | + - `token` |
| 70 | + - `securePaymentUrl` |
| 71 | + |
| 72 | + This mapping lets you reconcile webhook events back to your internal records. |
| 73 | + </Step> |
| 74 | + |
| 75 | + <Step title="Redirect payer to secure page"> |
| 76 | + Redirect in the same tab or open the secure URL in a new tab. |
| 77 | + </Step> |
| 78 | + |
| 79 | + <Step title="Process webhook events and update order status"> |
| 80 | + Handle payment events from webhooks and update your order/payment state from those events. |
| 81 | + </Step> |
| 82 | +</Steps> |
| 83 | + |
| 84 | +## Integration pattern: generated URL + redirect |
| 85 | + |
| 86 | +### Backend example (Node.js/Express) |
| 87 | + |
| 88 | +```javascript server.js |
| 89 | +import express from "express"; |
| 90 | + |
| 91 | +const app = express(); |
| 92 | +app.use(express.json()); |
| 93 | + |
| 94 | +app.post("/api/checkout/secure-payment", async (req, res) => { |
| 95 | + const { orderId, payee, amount, currencyId } = req.body; |
| 96 | + |
| 97 | + const apiResponse = await fetch("https://api.request.network/v2/secure-payments", { |
| 98 | + method: "POST", |
| 99 | + headers: { |
| 100 | + "x-api-key": process.env.REQUEST_API_KEY, |
| 101 | + "content-type": "application/json", |
| 102 | + }, |
| 103 | + body: JSON.stringify({ |
| 104 | + requests: [ |
| 105 | + { |
| 106 | + payee, |
| 107 | + amount, |
| 108 | + invoiceCurrency: currencyId, |
| 109 | + paymentCurrency: currencyId, |
| 110 | + }, |
| 111 | + ], |
| 112 | + }), |
| 113 | + }); |
| 114 | + |
| 115 | + if (!apiResponse.ok) { |
| 116 | + const errorBody = await apiResponse.text(); |
| 117 | + return res.status(apiResponse.status).json({ error: errorBody }); |
| 118 | + } |
| 119 | + |
| 120 | + const securePayment = await apiResponse.json(); |
| 121 | + |
| 122 | + // Persist in your DB |
| 123 | + // Example payload: |
| 124 | + // { |
| 125 | + // orderId, |
| 126 | + // requestIds: securePayment.requestIds, |
| 127 | + // token: securePayment.token, |
| 128 | + // securePaymentUrl: securePayment.securePaymentUrl, |
| 129 | + // status: "pending" |
| 130 | + // } |
| 131 | + |
| 132 | + return res.status(200).json({ |
| 133 | + orderId, |
| 134 | + securePaymentUrl: securePayment.securePaymentUrl, |
| 135 | + }); |
| 136 | +}); |
| 137 | +``` |
| 138 | + |
| 139 | +### Frontend redirect examples |
| 140 | + |
| 141 | +<CodeGroup> |
| 142 | +```javascript Same tab |
| 143 | +window.location.href = securePaymentUrl; |
| 144 | +``` |
| 145 | + |
| 146 | +```javascript New tab |
| 147 | +window.open(securePaymentUrl, "_blank", "noopener,noreferrer"); |
| 148 | +``` |
| 149 | +</CodeGroup> |
| 150 | + |
| 151 | +## Payment status updates with webhooks |
| 152 | + |
| 153 | +Use webhook events as your payment status source of truth. |
| 154 | + |
| 155 | +Typical mapping: |
| 156 | +- `payment.confirmed` -> mark order as paid |
| 157 | +- `payment.partial` -> mark order as partially paid |
| 158 | +- `payment.failed` -> mark order as failed |
| 159 | + |
| 160 | +### Webhook handler example (signature verification + reconciliation) |
| 161 | + |
| 162 | +```javascript webhook.js |
| 163 | +import crypto from "node:crypto"; |
| 164 | +import express from "express"; |
| 165 | + |
| 166 | +const app = express(); |
| 167 | + |
| 168 | +app.use( |
| 169 | + express.raw({ |
| 170 | + type: "application/json", |
| 171 | + verify: (req, _res, buf) => { |
| 172 | + req.rawBody = buf; |
| 173 | + }, |
| 174 | + }), |
| 175 | +); |
| 176 | + |
| 177 | +app.post("/webhooks/request", async (req, res) => { |
| 178 | + const signature = req.headers["x-request-network-signature"]; |
| 179 | + const secret = process.env.REQUEST_WEBHOOK_SECRET; |
| 180 | + |
| 181 | + const expectedSignature = crypto |
| 182 | + .createHmac("sha256", secret) |
| 183 | + .update(req.rawBody) |
| 184 | + .digest("hex"); |
| 185 | + |
| 186 | + if (!signature) { |
| 187 | + return res.status(401).json({ error: "Missing signature" }); |
| 188 | + } |
| 189 | + |
| 190 | + try { |
| 191 | + const isValid = crypto.timingSafeEqual( |
| 192 | + Buffer.from(signature), |
| 193 | + Buffer.from(expectedSignature), |
| 194 | + ); |
| 195 | + if (!isValid) { |
| 196 | + return res.status(401).json({ error: "Invalid signature" }); |
| 197 | + } |
| 198 | + } catch { |
| 199 | + return res.status(401).json({ error: "Invalid signature format" }); |
| 200 | + } |
| 201 | + |
| 202 | + const event = JSON.parse(req.rawBody.toString("utf8")); |
| 203 | + const requestId = event.requestId || event.requestID; |
| 204 | + |
| 205 | + // Find internal record by requestId in your DB, then update order status. |
| 206 | + // Example: |
| 207 | + // const checkout = await db.findCheckoutByRequestId(requestId) |
| 208 | + // if (event.event === "payment.confirmed") await db.markPaid(checkout.orderId) |
| 209 | + |
| 210 | + return res.status(200).json({ received: true }); |
| 211 | +}); |
| 212 | +``` |
| 213 | + |
| 214 | +## Expiry handling |
| 215 | + |
| 216 | +Secure payment links expire after one week by default. |
| 217 | + |
| 218 | +If a payer opens an expired link, create a new secure payment link and redirect again. |
| 219 | + |
| 220 | +## Troubleshooting |
| 221 | + |
| 222 | +<AccordionGroup> |
| 223 | + <Accordion title="401 Unauthorized when creating secure payments"> |
| 224 | + - Verify your `x-api-key` or `x-client-id` header |
| 225 | + - If using Client ID in browser, verify the request origin is in allowed domains |
| 226 | + </Accordion> |
| 227 | + |
| 228 | + <Accordion title="403 when loading secure payment token"> |
| 229 | + - The token may be expired |
| 230 | + - Create a fresh secure payment link and retry |
| 231 | + </Accordion> |
| 232 | + |
| 233 | + <Accordion title="409 already completed"> |
| 234 | + - Payment is already completed |
| 235 | + - Show a paid/completed state in your app instead of retrying payment |
| 236 | + </Accordion> |
| 237 | + |
| 238 | + <Accordion title="Webhook events not updating order status"> |
| 239 | + - Verify HMAC signature validation uses raw request body |
| 240 | + - Ensure your endpoint returns `2xx` after successful processing |
| 241 | + - Confirm your DB lookup maps incoming `requestId`/`requestID` to stored request IDs |
| 242 | + </Accordion> |
| 243 | +</AccordionGroup> |
| 244 | + |
| 245 | +## Related docs |
| 246 | + |
| 247 | +<CardGroup cols={3}> |
| 248 | + <Card title="Secure Payments API Reference" href="/api-reference/secure-payments" icon="book"> |
| 249 | + Full request and response schema details. |
| 250 | + </Card> |
| 251 | + |
| 252 | + <Card title="Webhooks" href="/api-reference/webhooks" icon="webhook"> |
| 253 | + Event types, signing, retries, and payload details. |
| 254 | + </Card> |
| 255 | + |
| 256 | + <Card title="Authentication" href="/api-reference/authentication" icon="key"> |
| 257 | + API key and Client ID setup. |
| 258 | + </Card> |
| 259 | +</CardGroup> |
0 commit comments