curl --request POST \
--url https://api-staging.rolla.xyz/api/v1/external/wallet/transactions/{transactionId}/decision \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"action": "approve"
}
'{
"status": 200,
"message": "Transaction approved successfully",
"success": true,
"data": {
"transaction": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"transaction_type": "withdrawal",
"status": "completed",
"description": "Vendor payment",
"external_reference": "ref-abc123",
"fee_amount": 53,
"source_amount": 500000,
"destination_amount": 499947,
"source_currency": "NGN",
"destination_currency": "NGN",
"created_at": "2024-01-15T10:30:00.000Z",
"updated_at": "2024-01-15T10:30:45.000Z",
"exchange_rate": 1480,
"virtual_account": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"currency": "USD",
"method": "bank_transfer",
"bank_name": "Lead Bank",
"account_number": "9876543210",
"account_name": "Acme Ltd / Rolla",
"reference": "<string>",
"label": "<string>",
"routing_number": "021000021",
"swift_code": "LEADUS33",
"wallet_address": "<string>",
"network": "base",
"provider": "MTN",
"phone_number": "<string>"
},
"payer": {
"name": "Acme Trading Ltd",
"account_number": "5551234567",
"bank_name": "HSBC",
"country": "Hong Kong"
},
"payment_rail": "wire_international",
"fee_paid_by": "account",
"fee_transaction_id": "c1b2a3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"fee_reference": "FEE-8KD02M1X",
"related_transaction_id": "b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"related_reference": "NG-J46RHHKV",
"fee_breakdown": [
{
"party": "platform",
"amount": 140,
"transaction_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"reference": "FEE-8KD02M1X"
}
],
"uetr": "97ed4827-7b6f-4491-a06f-b548d5a7512d",
"tracking_codes": {
"UETR": "97ed4827-7b6f-4491-a06f-b548d5a7512d",
"IMAD": "20260818B1QGC01C000123"
},
"metadata": {},
"beneficiary": {
"account_name": "JOHN DOE",
"bank_name": "Access Bank",
"bank_code": "000014",
"account_number": "0123456789"
}
}
}
}{
"status": 400,
"message": "Transaction is not pending or sent: completed"
}{
"status": 400,
"message": "Validation failed",
"code": "ACCOUNT_RESTRICTED",
"errors": [
{
"path": [
"amount"
],
"message": "amount must be a whole number of smallest currency units (e.g. cents)"
}
]
}{
"status": 404,
"message": "Transaction not found: 00000000-0000-4000-8000-000000000000"
}Approve or Reject a Transaction (Sandbox)
Sandbox only: settles or rejects a transaction that is waiting on a decision, so you do not have to ask the Rolla team to clear your test transactions by hand.
No provider confirms a transfer in sandbox, so simulated deposits and payouts stay pending until somebody decides them. approve runs the same settlement Rolla’s own operators run — the ledger posts, related fee transactions settle with it, the confirmation email is sent and the transaction.completed webhook fires. reject discards the entries, moves the transaction to rejected and fails its related fee transactions, so a failed deposit costs nothing.
Works for every currency. The route does not exist in production, where it returns 404.
curl --request POST \
--url https://api-staging.rolla.xyz/api/v1/external/wallet/transactions/{transactionId}/decision \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"action": "approve"
}
'{
"status": 200,
"message": "Transaction approved successfully",
"success": true,
"data": {
"transaction": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"transaction_type": "withdrawal",
"status": "completed",
"description": "Vendor payment",
"external_reference": "ref-abc123",
"fee_amount": 53,
"source_amount": 500000,
"destination_amount": 499947,
"source_currency": "NGN",
"destination_currency": "NGN",
"created_at": "2024-01-15T10:30:00.000Z",
"updated_at": "2024-01-15T10:30:45.000Z",
"exchange_rate": 1480,
"virtual_account": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"currency": "USD",
"method": "bank_transfer",
"bank_name": "Lead Bank",
"account_number": "9876543210",
"account_name": "Acme Ltd / Rolla",
"reference": "<string>",
"label": "<string>",
"routing_number": "021000021",
"swift_code": "LEADUS33",
"wallet_address": "<string>",
"network": "base",
"provider": "MTN",
"phone_number": "<string>"
},
"payer": {
"name": "Acme Trading Ltd",
"account_number": "5551234567",
"bank_name": "HSBC",
"country": "Hong Kong"
},
"payment_rail": "wire_international",
"fee_paid_by": "account",
"fee_transaction_id": "c1b2a3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"fee_reference": "FEE-8KD02M1X",
"related_transaction_id": "b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"related_reference": "NG-J46RHHKV",
"fee_breakdown": [
{
"party": "platform",
"amount": 140,
"transaction_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"reference": "FEE-8KD02M1X"
}
],
"uetr": "97ed4827-7b6f-4491-a06f-b548d5a7512d",
"tracking_codes": {
"UETR": "97ed4827-7b6f-4491-a06f-b548d5a7512d",
"IMAD": "20260818B1QGC01C000123"
},
"metadata": {},
"beneficiary": {
"account_name": "JOHN DOE",
"bank_name": "Access Bank",
"bank_code": "000014",
"account_number": "0123456789"
}
}
}
}{
"status": 400,
"message": "Transaction is not pending or sent: completed"
}{
"status": 400,
"message": "Validation failed",
"code": "ACCOUNT_RESTRICTED",
"errors": [
{
"path": [
"amount"
],
"message": "amount must be a whole number of smallest currency units (e.g. cents)"
}
]
}{
"status": 404,
"message": "Transaction not found: 00000000-0000-4000-8000-000000000000"
}pending until somebody decides it. This endpoint is that decision, and it runs the same settlement path Rolla’s own operators run — the behaviour you test against is the behaviour you get in production.
404 Not Found.Request Body
| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | approve to settle the transaction, reject to discard it. Any other value returns 400. |
reason | string | No | Why it was rejected, up to 500 characters. Recorded on the transaction and included in the rejection email. Ignored for approve. |
Approving
curl -X POST "https://api-staging.rolla.xyz/api/v1/external/wallet/transactions/357cf04f-2096-4b45-9c14-e33db131b5dd/decision" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "action": "approve" }'
{
"status": 200,
"message": "Transaction approved successfully",
"success": true,
"data": { "transaction": { "id": "357cf04f-2096-4b45-9c14-e33db131b5dd", "status": "completed" } }
}
transaction.completed webhook.
Rejecting
curl -X POST "https://api-staging.rolla.xyz/api/v1/external/wallet/transactions/565aa698-af83-4a9b-9082-a412ecd419b6/decision" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "action": "reject", "reason": "simulating a failed inbound wire" }'
{
"status": 200,
"message": "Transaction rejected successfully",
"success": true,
"data": { "transaction": { "id": "565aa698-af83-4a9b-9082-a412ecd419b6", "status": "rejected" } }
}
rejected. The reason is sent to the account’s notification recipients, exactly as a Rolla-side rejection would be.
Fees settle with the transaction
A deposit or payout usually carries fee transactions alongside it. They follow the decision automatically — you never decide them separately:| Decision | The transaction | Its fee transactions |
|---|---|---|
approve | completed | completed — fees are charged |
reject | rejected | failed, entries discarded — nothing is charged |
Which transactions you can decide
Any transaction in your own book, plus — if you are a platform partner — any belonging to an account in your tenancy. It is the same set List Transactions returns, so anything you can read, you can decide. A transaction outside that set reports the same404 as one that does not exist, so an id cannot be used to probe another partner’s book.
When a decision is refused
| Status | Why |
|---|---|
400 | action missing or not approve/reject — "Validation failed" |
400 | Already decided: "Transaction is not pending or sent: completed" |
400 | A Rolla operator currently holds the transaction, or it is a Mobile Money collection awaiting its provider callback |
404 | No such transaction in your book |
pending, sent or processing can be decided. Deciding one twice is refused rather than repeated, so a retry after a timeout is safe.
pending, then decide it.Who decided it is recorded
Every decision is attributed, the same way a Rolla operator’s is. The transaction records the action, the time, the reason, and the API key that made the call — so when a business holds several keys you can still tell which integration settled what.A full sandbox loop
Simulate the money arriving
Wait for the transaction
pending.Decide it
POST /wallet/transactions/{transactionId}/decision with approve or reject.Check what you expected
transaction.completed.Authorizations
Your Rolla API key
Path Parameters
Rolla's transaction ID (UUID), as returned by List Transactions.
Body
approve settles the transaction; reject discards it. Any other value is rejected with 400 Validation failed.
approve, reject "approve"
Why the transaction was rejected. Recorded on the transaction and included in the rejection email sent to the account's notification recipients. Ignored for approve.
1 - 500"simulating a failed inbound wire"