|
| 1 | +# Transak On-Ramp Backend API |
| 2 | + |
| 3 | +This document describes the backend required by vechain-kit's fiat on-ramp feature (`PayWithTransakButton`, `TransakCheckoutModal`). |
| 4 | + |
| 5 | +## Why a backend is required |
| 6 | + |
| 7 | +Transak deprecated direct widget URLs (API key in the query string). Widgets now only load from a **Secure Widget URL** minted by Transak's Create Widget URL API, which is authenticated with your **API secret**. That secret must never be exposed in the browser, so a small server endpoint is required. |
| 8 | + |
| 9 | +The flow is: |
| 10 | + |
| 11 | +```text |
| 12 | +Browser (vechain-kit) Your backend Transak |
| 13 | +┌──────────────────┐ POST ┌──────────────────┐ POST ┌──────────────────┐ |
| 14 | +│ widgetUrlBuilder │ ───────► │ /api/transak/ │ ──────► │ /api/v2/ │ |
| 15 | +│ (kit passes │ │ widget-url │ ①token │ auth/session │ |
| 16 | +│ walletAddress, │ │ │ │ │ |
| 17 | +│ fiat, env...) │ │ ② POST widget │ ◄────── │ returns │ |
| 18 | +│ │ │ (server-to- │ widget │ secure widget │ |
| 19 | +│ │ ◄─────── │ server) │ URL │ URL (5 min TTL) │ |
| 20 | +└──────────────────┘ widget └──────────────────┘ └──────────────────┘ |
| 21 | + URL returned ▲ |
| 22 | + └─► Transak SDK loads │ ① access token via POST /partners/api/v2/refresh-token |
| 23 | + │ (7-day validity, cache it) |
| 24 | +``` |
| 25 | + |
| 26 | +## 1. Create Transak API credentials |
| 27 | + |
| 28 | +1. Go to the [Transak Partner Dashboard](https://dashboard.transak.com) and create a partner account. |
| 29 | +2. Generate an **API key** and **API secret**. Credentials are environment-specific: |
| 30 | + - **Sandbox** credentials (for testing) — work against the `staging` endpoints. |
| 31 | + - **Production** credentials — work against the `production` endpoints. |
| 32 | +3. Add the `referrerDomain` of your app (the domain the widget will be served from) as an allowed referrer, otherwise session creation fails with `403`. |
| 33 | +4. Transak requires the end user's real IP on its authenticated APIs (`x-user-ip` header) — your server must forward the caller's IP. |
| 34 | + |
| 35 | +## 2. Server and client environment variables |
| 36 | + |
| 37 | +| Variable | Scope | Description | |
| 38 | +| --- | --- | --- | |
| 39 | +| `TRANSAK_API_KEY` | Server only | Partner API key. Never in the browser bundle. | |
| 40 | +| `TRANSAK_API_SECRET` | Server only | Partner API secret. Keep it on the backend only. | |
| 41 | +| `NEXT_PUBLIC_TRANSAK_API_KEY` | Client | Same API key, safe to expose (works only with the server-side secret). | |
| 42 | +| `NEXT_PUBLIC_TRANSAK_API_URL` | Client | Base URL of your mini-server (leave empty for same-origin). | |
| 43 | +| `NEXT_PUBLIC_TRANSAK_ENVIRONMENT` | Client | `'staging'` or `'production'`. Overrides the kit's auto-derived value. Set to `'staging'` for preview deployments and `'production'` for live. | |
| 44 | + |
| 45 | +## 3. Endpoint contract |
| 46 | + |
| 47 | +### `POST /api/transak/widget-url` |
| 48 | + |
| 49 | +**Request:** |
| 50 | +```json |
| 51 | +{ |
| 52 | + "environment": "staging", |
| 53 | + "widgetParams": { |
| 54 | + "apiKey": "your_transak_api_key", |
| 55 | + "referrerDomain": "https://yourapp.com", |
| 56 | + "walletAddress": "0x...", |
| 57 | + "fiatAmount": "10", |
| 58 | + "fiatCurrency": "USD", |
| 59 | + "cryptoCurrencyCode": "VET", |
| 60 | + "network": "vechain", |
| 61 | + "defaultCryptoCurrency": "VET", |
| 62 | + "disableWalletAddressForm": true |
| 63 | + } |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +| Field | Description | |
| 68 | +| --- | --- | |
| 69 | +| `environment` | Which Transak environment to use. vechain-kit derives it automatically from the connected VeChain network (`main` → `production`, `test`/`solo` → `staging`) unless overridden via `TransakConfig.environment` on `VeChainKitProvider`. Your endpoint should trust this value and select the matching Transak base URLs. | |
| 70 | +| `widgetParams` | Transak widget configuration, see the [Transak query parameters docs](https://docs.transak.com/customization/query-parameters). | |
| 71 | + |
| 72 | +**Server behavior:** |
| 73 | + |
| 74 | +1. Obtain a partner access token via `POST https://api-stg.transak.com/partners/api/v2/refresh-token` (staging) or `POST https://api.transak.com/partners/api/v2/refresh-token` (production), with headers `x-api-key` and `api-secret` and body `{ "apiKey": ... }`. The token is valid for **7 days** — cache it and reuse it. Cache per environment; a staging token must never be sent to production endpoints. |
| 75 | +2. Mint the widget session via `POST https://api-gateway-stg.transak.com/api/v2/auth/session` (staging) or `POST https://api-gateway.transak.com/api/v2/auth/session` (production), with headers `x-api-key`, `access-token` and `x-user-ip` (the caller's real IP, e.g. from `x-forwarded-for`), and body `{ "widgetParams": ... }`. |
| 76 | +3. Return Transak's response. |
| 77 | + |
| 78 | +**Response (200):** |
| 79 | +```json |
| 80 | +{ |
| 81 | + "data": { |
| 82 | + "widgetUrl": "https://global-stg.transak.com/...?sessionId=..." |
| 83 | + } |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +The widget URL is valid for **5 minutes** and single-use — mint a fresh one per checkout. |
| 88 | + |
| 89 | +## 4. Wire it into vechain-kit |
| 90 | + |
| 91 | +```tsx |
| 92 | +<VeChainKitProvider |
| 93 | + transak={{ |
| 94 | + apiKey: process.env.NEXT_PUBLIC_TRANSAK_API_KEY, |
| 95 | + widgetUrlBuilder: async ({ |
| 96 | + walletAddress, |
| 97 | + fiatAmount, |
| 98 | + fiatCurrency, |
| 99 | + cryptoCurrency, |
| 100 | + network, |
| 101 | + }) => { |
| 102 | + const res = await fetch('/api/transak/widget-url', { |
| 103 | + method: 'POST', |
| 104 | + headers: { 'Content-Type': 'application/json' }, |
| 105 | + body: JSON.stringify({ |
| 106 | + environment: process.env.NEXT_PUBLIC_TRANSAK_ENVIRONMENT as |
| 107 | + | 'staging' |
| 108 | + | 'production' |
| 109 | + | undefined, |
| 110 | + widgetParams: { |
| 111 | + apiKey: process.env.NEXT_PUBLIC_TRANSAK_API_KEY, |
| 112 | + referrerDomain: window.location.origin, |
| 113 | + walletAddress, |
| 114 | + fiatAmount, |
| 115 | + fiatCurrency, |
| 116 | + cryptoCurrencyCode: cryptoCurrency, |
| 117 | + network, |
| 118 | + defaultCryptoCurrency: 'VET', |
| 119 | + disableWalletAddressForm: true, |
| 120 | + }, |
| 121 | + }), |
| 122 | + }); |
| 123 | + const json = await res.json(); |
| 124 | + if (!res.ok) throw new Error(json?.error ?? 'Failed to create Transak widget URL'); |
| 125 | + return json.data.widgetUrl; |
| 126 | + }, |
| 127 | + }} |
| 128 | +> |
| 129 | +``` |
| 130 | + |
| 131 | +The `environment` parameter is passed through to your `widgetUrlBuilder` — your backend uses it to select the correct Transak endpoints. Set `transak.environment` explicitly (e.g. `NEXT_PUBLIC_TRANSAK_ENVIRONMENT='staging'` for previews, `'production'` for live) when you want to force the value regardless of the connected VeChain network. Omit it to let the kit auto-derive (`main` → `production`, `test`/`solo` → `staging`). |
| 132 | + |
| 133 | +## 5. Reference implementation |
| 134 | + |
| 135 | +A complete, working Next.js implementation ships in the repo: |
| 136 | + |
| 137 | +- Route: `examples/next-template/src/app/api/transak/widget-url/route.ts` |
| 138 | +- Provider wiring: `examples/next-template/src/app/providers/VechainKitProviderWrapper.tsx` |
| 139 | +- Env vars: `examples/next-template/.env.example` |
| 140 | + |
| 141 | +## 6. Mandatory Transak integration requirements |
| 142 | + |
| 143 | +These are **architectural prerequisites** for any production deployment of the on-ramp feature, regardless of the backend framework you choose. Transak enforces them server-side; missing any of them results in `403` / `401` errors from the Transak API. |
| 144 | + |
| 145 | +### 6.1 Credential isolation |
| 146 | + |
| 147 | +| Property | Value | Where | |
| 148 | +| --- | --- | --- | |
| 149 | +| `TRANSAK_API_KEY` | Partner API key | Server env only — never in the browser bundle | |
| 150 | +| `TRANSAK_API_SECRET` | Partner API secret | Server env only — never sent to the client under any circumstance | |
| 151 | +| `NEXT_PUBLIC_TRANSAK_API_KEY` | Same API key, safe to expose | Client bundle (used by the kit to pass through to your backend) | |
| 152 | + |
| 153 | +The **API key** is safe to ship in the client bundle — it only works together with the **API secret**, which must stay server-side. The kit never needs the secret. |
| 154 | + |
| 155 | +### 6.2 `x-api-key` header (server-to-Transak only) |
| 156 | + |
| 157 | +Transak requires `x-api-key` on every authenticated API call (`refresh-token` and `auth/session`). Set it from `TRANSAK_API_KEY` **on the backend**, never from a client-supplied value. The `apiKey` in `widgetParams` is the partner-config value for the widget session, not the authentication key — do not conflate them. |
| 158 | + |
| 159 | +### 6.3 `x-user-ip` header (forward the real caller IP) |
| 160 | + |
| 161 | +Transak needs the end user's real IP for fraud detection and geo-compliance. Your backend **must** forward it as `x-user-ip` on the `auth/session` call. Use the leftmost public IP from: |
| 162 | + |
| 163 | +``` |
| 164 | +x-forwarded-for: 203.0.113.42, 10.0.0.1 |
| 165 | +``` |
| 166 | + |
| 167 | +Do not send your server's own IP — Transak will reject shared/NAT IPs in production. If you run behind a CDN or proxy, ensure the chain preserves the original client IP (e.g. CloudFront → `X-Forwarded-For`, ALB → `X-Forwarded-For`). |
| 168 | + |
| 169 | +### 6.4 Backend IP whitelisting |
| 170 | + |
| 171 | +Transak's production API can be locked to a set of **whitelisted source IPs** (configured in the Partner Dashboard under *API Settings → IP Whitelisting*). Your backend must have a **fixed egress IP** (or a small known range). This is mandatory for production — staging does not enforce it. |
| 172 | + |
| 173 | +Checklist: |
| 174 | + |
| 175 | +- [ ] Deploy the backend on infrastructure with a static NAT / elastic IP (Lambda behind API Gateway, a fixed-IP container, or a VPS with a static IP) |
| 176 | +- [ ] Add every backend egress IP to the Transak IP whitelist in the Partner Dashboard |
| 177 | +- [ ] If you use a serverless platform with rotating IPs (e.g. Vercel functions without a NAT gateway), put a proxy with a fixed IP in front, or move the endpoint to infra that provides one |
| 178 | + |
| 179 | +### 6.5 CORS — restrict to your domains only |
| 180 | + |
| 181 | +Your `/api/transak/widget-url` endpoint must set CORS headers that allow **only your own origins**: |
| 182 | + |
| 183 | +```http |
| 184 | +Access-Control-Allow-Origin: https://yourapp.com |
| 185 | +Access-Control-Allow-Methods: POST |
| 186 | +Access-Control-Allow-Headers: Content-Type |
| 187 | +``` |
| 188 | + |
| 189 | +Never use `Access-Control-Allow-Origin: *` — any website could then mint Transak sessions on your billing account. For multiple environments, maintain an explicit allow-list: |
| 190 | + |
| 191 | +```js |
| 192 | +const ALLOWED_ORIGINS = new Set([ |
| 193 | + 'https://yourapp.com', |
| 194 | + 'https://preview.vechainkit.vechain.org', |
| 195 | +]); |
| 196 | +const origin = req.headers.get('origin'); |
| 197 | +if (ALLOWED_ORIGINS.has(origin)) { |
| 198 | + res.headers.set('Access-Control-Allow-Origin', origin); |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +The `referrerDomain` sent in `widgetParams` must also match one of the domains registered in your Transak Partner Dashboard (see step 3 in section 1 above). Transak checks it server-side; a mismatch yields `403`. |
| 203 | + |
| 204 | +### 6.6 Rate limiting |
| 205 | + |
| 206 | +Your endpoint is unauthenticated from the client's perspective — protect it with an in-memory or distributed rate limit so a single caller cannot mint unlimited Transak sessions against your credentials. The reference implementation in the repo uses a simple in-memory cap (20 requests/minute/IP). For horizontally-scaled backends, use a shared store (Redis, DynamoDB, etc.). |
| 207 | + |
| 208 | +### 6.7 Partner onboarding checklist |
| 209 | + |
| 210 | +Before going live, confirm every item in the Transak Partner Dashboard: |
| 211 | + |
| 212 | +- [ ] Production API key + secret generated (not the sandbox ones) |
| 213 | +- [ ] All frontend domains added to **Allowed Referrer Domains** (production + any preview subdomains) |
| 214 | +- [ ] All backend egress IPs added to **IP Whitelisting** (production only) |
| 215 | +- [ ] KYC/AML settings configured (required countries, default fiat currency, default crypto) |
| 216 | +- [ ] Wallet address validation enabled (`disableWalletAddressForm: true` — the kit passes the connected wallet address directly; do not let users type an arbitrary address) |
| 217 | +- [ ] Webhook URL configured (for order status callbacks — not handled by the kit, must be on your backend) |
| 218 | +- [ ] Test purchase completed end-to-end on staging before switching to production credentials |
0 commit comments