Merchant Onboarding
This guide is for merchant engineers integrating HMAC /v1/* against
https://api.transacty.ai. Portal JWT (/portal/*) is a different product — do
not mix it into this integration.
What you get from Transacty
When a merchant is onboarded, they receive:
apiKeysecretbaseUrl—https://api.transacty.ai
These credentials are used to call the Transacty APIs.
There is no separate sandbox hostname. Create a test key or a live key
in the dashboard. Every /v1 call runs in that key’s environment (separate wallets
and transactions).
Base URL
https://api.transacty.ai
Test and live use the same base URL and paths. The API key chooses the environment.
Headers (every /v1/* request)
| Header | Value |
|---|---|
X-Transacty-Key | API key (transacty_…) |
X-Transacty-Timestamp | Unix time in seconds (not ms) |
X-Transacty-Signature | HMAC-SHA256 of {timestamp}.{rawBody}, lowercase hex |
Content-Type | application/json on POST/PATCH |
Idempotency-Key | Required on money writes (create pay-in / payout / payment intent / NGN virtual-account provision). Unique per create. Replaying the same key + same body returns the first response. |
Signing payload = timestamp + a literal . + the exact raw body bytes.
- POST/PATCH: sign the JSON string you send (no re-serialize after signing).
- GET: body is empty → payload is
{timestamp}.(trailing dot). - Replay window: ±5 minutes. Sign at send time; do not cache signatures.
import crypto from "node:crypto";
function sign(secret, timestamp, rawBody) {
return crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`, "utf8").digest("hex");
}
const timestamp = Math.floor(Date.now() / 1000).toString();
const rawBody = JSON.stringify({ amount: "500.00", paymentMethodCode: "BKASH" /* … */ });
const signature = sign(process.env.TRANSACTY_SECRET, timestamp, rawBody);
await fetch("https://api.transacty.ai/v1/payins", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Transacty-Key": process.env.TRANSACTY_KEY,
"X-Transacty-Timestamp": timestamp,
"X-Transacty-Signature": signature,
"Idempotency-Key": crypto.randomUUID(),
},
body: rawBody,
});Missing Idempotency-Key on a create → 400 { "error": "Bad Request", "message": "Idempotency-Key header is required" }.
Create pay-ins and payouts from your server only (HMAC). The dashboard uses a separate login (JWT) and never holds the API secret.
Scopes
Typical live key: payin:create, payout:create, balance:read. * is for
non-production only.
Markets
Each region is a separate KYB market (bangladesh, brazil, india, europe,
nigeria, pyusd). Until that market is approved, those /v1 routes return
403 with code: "market_not_enabled".
Status values
Use lowercase: pending, success, failed. Do not use PENDING / SUCCESS as
Transacty statuses.
Settlement wallets (never mix)
| Region | Collect | Merchant pocket (currency) | Display |
|---|---|---|---|
| Bangladesh | BDT e-wallets | BDT | BDT |
| Brazil | PIX | BRL | BRL |
| India H2H | INR via UPI | USDT | USDT |
| Europe | EUR/GBP Open Banking | USDC | USDC |
| Nigeria | Permanent NGN virtual account | NGN | NGN |
| PYUSD | PYUSD on Ethereum | PYUSD-USDC | PYUSD USDC |
PYUSD USDC is not Europe USDC. EUR payouts and Europe spend debit USDC only.
They cannot spend PYUSD-USDC. Do not treat PYUSD as settling to your Europe USDC
wallet.
Never sum unlike currencies into one headline number.
Test vs live
| test | live | |
|---|---|---|
| How you choose | Test API key | Live API key |
| Portal | environment: "test" | environment: "live" |
| Money | Isolated ledger | Production money |
| PYUSD | Not available (503 payment_unavailable) | Only supported mode |
| Nigeria (NGN) | Not available (503 payment_unavailable) | Only supported mode |
Bangladesh availability
POST /v1/payins and POST /v1/payouts may return 503
{ "code": "payment_unavailable" } while the Bangladesh rail is paused. Brazil,
India, Europe, Nigeria, and PYUSD are independent. Do not use Bangladesh as the only
integration test while that pause is on.
Common errors
| HTTP | code | Meaning |
|---|---|---|
| 400 | — | Validation, or missing Idempotency-Key |
| 401 | — | Bad key, signature, or timestamp outside ±5 min |
| 403 | market_not_enabled | That region is not approved |
| 403 | — | Missing scope |
| 503 | payment_unavailable | Rail paused, upstream down, or NGN / PYUSD test key |
| 409 | — | Same Idempotency-Key with a different body |
Regions and rails
The same API key and the same transaction list / webhook URL serve all rails — only the API paths and checkout UX differ.
| Region | Rail | Pay-in API | Settlement wallet | Checkout |
|---|---|---|---|---|
| Bangladesh | bangladesh | /v1/payins | BDT | Provider link from paymentInfo.content |
| Brazil | brazil | /v1/br/payins | BRL | PIX from paymentInfo |
| India | india | /v1/h2h/* | USDT | Merchant-built UPI UI |
| Europe | europe | /v1/eur/* | USDC | Redirect to checkoutUrl |
| Nigeria | nigeria | /v1/ngn/* | NGN | Permanent virtual account (share bank details) |
| PYUSD | pyusd | /v1/pyusd/payment-intents | PYUSD-USDC | Ethereum deposit address |
- Bangladesh: Pay-in / Payout
- Brazil: Pay-in / Payout
- India: Pay-in / Payout
- Europe: Pay-in / Payout
- Nigeria: Overview
- PYUSD: Overview
Main endpoints merchants use
Auth / identity
GET /v1/me
Balance
GET /v1/balance
Bangladesh pay-in / payout
POST /v1/payins·GET /v1/payins/:idPOST /v1/payouts·GET /v1/payouts/:id
Brazil PIX
POST /v1/br/payins·GET /v1/payins/:idPOST /v1/br/payouts·GET /v1/payouts/:id
India UPI (H2H)
POST /v1/h2h/payin-instances·GET /v1/h2h/payin-instances/:transactionIdPOST /v1/h2h/buyer-confirms-payment
Europe Open Banking
POST /v1/eur/payin-instances·GET /v1/eur/payin-instances/:transactionIdPOST /v1/eur/payout-instances·POST /v1/eur/payout-instances/:transactionId/approve
Nigeria (NGN virtual account)
GET /v1/ngn/virtual-account·POST /v1/ngn/virtual-accountGET /v1/ngn/banks·POST /v1/ngn/verify-accountPOST /v1/ngn/payouts·GET /v1/ngn/payouts/:transactionId
PYUSD (Ethereum → PYUSD USDC)
POST /v1/pyusd/payment-intents·GET /v1/pyusd/payment-intents/:transactionId
Transactions
GET /v1/transactions(filter bytype; then filterrail/statusin your app)GET /v1/transactions/:transactionId
Webhooks
PATCH /v1/me/webhook
Important notes for merchants
- Use
transactionIdfrom Transacty as your main reference. platformOrderIdis the provider-side reference (for support / escalation).- Do not call any payment provider directly from your integration.
- Each currency is a separate wallet pocket;
pendingBalanceis in-flight pay-ins. - Poll the relevant status endpoint when you need the latest state, or wait for webhooks.
First integration test
Do not rely on Bangladesh as the only smoke test while that rail may be paused
(503 payment_unavailable). Use a region whose market is approved:
- Brazil PIX —
POST /v1/br/payins - India UPI —
POST /v1/h2h/payin-instances - Europe Open Banking —
POST /v1/eur/payin-instances
Nigeria and PYUSD have no test mode. Use a live key only — a test key always
fails closed (503 payment_unavailable).
If Bangladesh is available:
Check identity
GET /v1/me
Check balance
GET /v1/balance
Create a pay-in
POST /v1/payins with Idempotency-Key, then complete the payer step from paymentInfo.content
Poll until success
GET /v1/payins/:id until success
Create a payout
POST /v1/payouts with Idempotency-Key, then GET /v1/payouts/:id until a final state
How to go live
Complete KYC
Activate your account in the portal.
Get each region approved
Each market (bangladesh, brazil, india, europe, nigeria, pyusd) is a
separate KYB approval.
Create API keys
Create a test key, then a live key when ready. Grant payin:create,
payout:create, and balance:read.
Sign every /v1 call
Send Idempotency-Key on creates (including NGN virtual-account provision).
Configure your webhook
PATCH /v1/me/webhook (HTTPS). Handle payin.completed / payin.failed.
Nigeria and PYUSD are live-only
For Nigeria (NGN) and PYUSD use the live key only. Do not expect a test key to work.