RemitSandbox
RemitSandbox is a simulated international remittance API. Use it to build and test an integration against a realistic provider before connecting to a live system. All data is ephemeral and for testing only.
Overview
https://remit.palashonanton.in
Protocol
HTTPS only
Format
application/json for all request and response bodies
Authentication
None required in sandbox
Rate limit
1,000 requests per minute per IP address
ID format
All resource IDs are prefixed strings of the form prefix_xxxxxxxx
(e.g. cus_3a1b2c4d, ben_9f0e1a2b,
trf_5c6d7e8f). Always treat them as opaque strings.
Errors
All errors use a consistent envelope. HTTP status codes follow standard
conventions: 400 for malformed requests, 404 for unknown
resources, 422 for semantic validation failures,
429 for rate-limit hits.
{
"error": {
"code": "string — machine-readable error identifier",
"message": "string — human-readable explanation",
"field": "string — present when the error relates to a specific field"
}
}
| code | HTTP status | Meaning |
|---|---|---|
invalid_json | 400 | Request body is not valid JSON |
missing_field | 422 | A required field was omitted |
unknown_field | 422 | An unrecognised field was sent |
validation_error | 422 | A field value failed a rule |
country_not_supported | 422 | Only US customers are accepted |
invalid_ifsc | 422 | IFSC code is not 11 characters |
invalid_currency | 422 | Only USD is accepted |
amount_exceeds_limit | 422 | Transfer amount above maximum |
beneficiary_ownership | 422 | Beneficiary belongs to a different customer |
not_found | 404 | Resource ID does not exist |
rate_limit_exceeded | 429 | Too many requests from this IP |
Health
Returns {"ok": true} when the service is up. No authentication required.
{"ok": true}
Customers
A Customer represents the sender — a person in the United States initiating remittances. Only US-based senders are supported in this sandbox.
Create a new customer. The country field must be "US".
| Field | Type | Description | |
|---|---|---|---|
first_name | string | required | Legal first name |
last_name | string | required | Legal last name |
email | string | required | Valid email address (e.g. alice@example.com) |
phone | string | required | Phone number in E.164 format (e.g. +12025550182) |
country | string | required | ISO 3166-1 alpha-2 country code. Must be "US". |
{
"first_name": "Alice",
"last_name": "Sharma",
"email": "alice@example.com",
"phone": "+12025550182",
"country": "US"
}
{
"id": "cus_3a1b2c4d",
"first_name": "Alice",
"last_name": "Sharma",
"email": "alice@example.com",
"phone": "+12025550182",
"country": "US",
"created_at": "2026-01-15T10:30:00.000000+00:00"
}
Retrieve a customer by ID. Returns 404 if the ID is unknown.
{
"id": "cus_3a1b2c4d",
"first_name": "Alice",
"last_name": "Sharma",
"email": "alice@example.com",
"phone": "+12025550182",
"country": "US",
"created_at": "2026-01-15T10:30:00.000000+00:00"
}
Beneficiaries
A Beneficiary is a recipient bank account in India, linked to a specific customer. Each beneficiary must be associated with an existing customer.
Add a beneficiary for a customer. Returns 404 if customer_id does not exist.
| Field | Type | Description | |
|---|---|---|---|
customer_id | string | required | ID of the owning customer |
name | string | required | Full name of the account holder |
account_no | string | required | Bank account number at the destination |
ifsc | string | required | 11-character IFSC code identifying the destination branch |
bank_name | string | required | Name of the destination bank |
{
"customer_id": "cus_3a1b2c4d",
"name": "Ramesh Kumar",
"account_no": "9876543210",
"ifsc": "HDFC0001234",
"bank_name": "HDFC Bank"
}
{
"id": "ben_9f0e1a2b",
"customer_id": "cus_3a1b2c4d",
"name": "Ramesh Kumar",
"account_no": "9876543210",
"ifsc": "HDFC0001234",
"bank_name": "HDFC Bank",
"created_at": "2026-01-15T10:31:00.000000+00:00"
}
Retrieve a beneficiary by ID. Returns 404 if the ID is unknown.
{
"id": "ben_9f0e1a2b",
"customer_id": "cus_3a1b2c4d",
"name": "Ramesh Kumar",
"account_no": "9876543210",
"ifsc": "HDFC0001234",
"bank_name": "HDFC Bank",
"created_at": "2026-01-15T10:31:00.000000+00:00"
}
Transfers
A Transfer moves USD from a customer's account to a linked beneficiary in India. The payout is calculated at a fixed exchange rate and expressed in INR.
Transfers are processed asynchronously. After creation the status will be
PROCESSING for a short period, then
settle to either COMPLETED or
FAILED. Poll
GET /transfers/{id} to track progress.
Initiate a new transfer. The beneficiary must belong to the given customer. Returns 404 if customer_id or beneficiary does not exist.
| Field | Type | Description | |
|---|---|---|---|
customer_id | string | required | ID of the sending customer |
beneficiary | string | required | ID of the destination beneficiary |
amount | number | required | Transfer amount in USD. Must be > 0 and ≤ 10,000. |
currency | string | required | Must be "USD" |
reference | string | required | Your internal reference string (not validated) |
{
"customer_id": "cus_3a1b2c4d",
"beneficiary": "ben_9f0e1a2b",
"amount": 250.00,
"currency": "USD",
"reference": "invoice-2026-042"
}
{
"id": "trf_5c6d7e8f",
"status": "PROCESSING",
"customer_id": "cus_3a1b2c4d",
"beneficiary_id": "ben_9f0e1a2b",
"amount": 250.0,
"currency": "USD",
"reference": "invoice-2026-042",
"fx_rate": 83.5,
"payout_amount_inr": 20875.0,
"created_at": "2026-01-15T10:32:00.000000+00:00"
}
Test trigger
Any transfer whose amount has a cents value of exactly .13
(e.g. 100.13, 250.13, 0.13) will always settle
as FAILED with failure_reason: "Beneficiary bank rejected the transfer".
Use this to reliably exercise your FAILED-transfer handling without waiting for the
random ~10% failure rate.
Retrieve the current status of a transfer. Poll this endpoint after creation to detect when processing is complete.
Status values
| Status | Meaning |
|---|---|
| PROCESSING | Transfer has been received and is being processed |
| COMPLETED | Funds have been delivered to the beneficiary |
| FAILED | Transfer could not be completed; see failure_reason |
{
"id": "trf_5c6d7e8f",
"status": "PROCESSING",
"customer_id": "cus_3a1b2c4d",
"beneficiary_id": "ben_9f0e1a2b",
"amount": 250.0,
"currency": "USD",
"reference": "invoice-2026-042",
"fx_rate": 83.5,
"payout_amount_inr": 20875.0,
"created_at": "2026-01-15T10:32:00.000000+00:00"
}
{
"id": "trf_5c6d7e8f",
"status": "COMPLETED",
"customer_id": "cus_3a1b2c4d",
"beneficiary_id": "ben_9f0e1a2b",
"amount": 250.0,
"currency": "USD",
"reference": "invoice-2026-042",
"fx_rate": 83.5,
"payout_amount_inr": 20875.0,
"created_at": "2026-01-15T10:32:00.000000+00:00"
}
{
"id": "trf_5c6d7e8f",
"status": "FAILED",
"customer_id": "cus_3a1b2c4d",
"beneficiary_id": "ben_9f0e1a2b",
"amount": 250.0,
"currency": "USD",
"reference": "invoice-2026-042",
"fx_rate": 83.5,
"payout_amount_inr": 20875.0,
"failure_reason": "Beneficiary bank rejected the transfer",
"created_at": "2026-01-15T10:32:00.000000+00:00"
}
Starter Code
Save this file as adapter.py and implement every method marked
TODO so that test_adapter.py passes.
Read the docs above carefully — some request field names differ from the
parameter names in the method signatures.
"""
RemitSandbox adapter — starter template.
Your task: implement every method marked TODO so that test_adapter.py passes.
python test_adapter.py --skip-wait # fast feedback loop
python test_adapter.py # full run including settlement (~65 s)
Read the API documentation at https://remit.palashonanton.in before you start.
Pay close attention to the request field names and response shapes.
Dependencies:
pip install requests
"""
import time
from typing import Optional
import requests # https://requests.readthedocs.io
BASE_URL = "https://remit.palashonanton.in"
class RemitSandboxError(Exception):
"""
Raised whenever the RemitSandbox API returns a non-2xx response.
Attributes:
status_code HTTP status code (e.g. 404, 422)
code Machine-readable error identifier from the response body
message Human-readable explanation from the response body
field The request field that caused the error, if any (may be None)
Example error response body:
{
"error": {
"code": "not_found",
"message": "Customer 'cus_abc123' not found.",
"field": "customer_id"
}
}
"""
def __init__(
self,
status_code: int,
code: str,
message: str,
field: Optional[str] = None,
):
self.status_code = status_code
self.code = code
self.message = message
self.field = field
detail = f" (field: {field})" if field else ""
super().__init__(f"HTTP {status_code} [{code}] {message}{detail}")
class RemitSandboxAdapter:
"""
Adapter for the RemitSandbox API.
All methods raise RemitSandboxError on API errors; let the caller decide
whether to retry or surface the error.
Usage:
adapter = RemitSandboxAdapter()
customer = adapter.create_customer("Alice", "Sharma", "alice@example.com", "+12025550182")
beneficiary = adapter.create_beneficiary(customer["id"], "Ramesh", "9876543210", "HDFC0001234", "HDFC Bank")
transfer = adapter.create_transfer(customer["id"], beneficiary["id"], 250.0)
result = adapter.poll_transfer(transfer["id"])
print(result["status"]) # "SUCCESS" or "FAILED"
"""
def __init__(self, base_url: str = BASE_URL):
self.base_url = base_url.rstrip("/")
# TODO: create a requests.Session and configure default headers.
# All requests and responses use JSON; set Content-Type accordingly.
raise NotImplementedError("TODO: initialise the HTTP session")
# ── Internal helper ───────────────────────────────────────────────────────
def _request(self, method: str, path: str, **kwargs) -> dict:
"""
Send an HTTP request to `self.base_url + path`.
Return the parsed JSON body on success (2xx).
Raise RemitSandboxError on any non-2xx response.
The error envelope is always:
{"error": {"code": "...", "message": "...", "field": "..."}}
where "field" may be absent.
"""
# TODO: make the request, parse the response, raise on errors
raise NotImplementedError("TODO: implement _request")
# ── Customers ─────────────────────────────────────────────────────────────
def create_customer(
self,
first_name: str,
last_name: str,
email: str,
phone: str,
country: str = "US",
) -> dict:
"""
Create a new customer. Returns the created customer dict.
The response includes at minimum: id, first_name, last_name, email,
phone, country, created_at.
Raises RemitSandboxError on validation failure (e.g. unsupported country,
invalid email or phone format).
"""
# TODO: POST /customers
raise NotImplementedError("TODO: implement create_customer")
def get_customer(self, customer_id: str) -> dict:
"""
Retrieve a customer by ID.
Raises RemitSandboxError(code="not_found") if the ID does not exist.
"""
# TODO: GET /customers/{customer_id}
raise NotImplementedError("TODO: implement get_customer")
# ── Beneficiaries ─────────────────────────────────────────────────────────
def create_beneficiary(
self,
customer_id: str,
name: str,
account_number: str,
ifsc: str,
bank_name: str,
) -> dict:
"""
Add a beneficiary linked to the given customer.
Returns the created beneficiary dict.
Raises RemitSandboxError on validation failure (e.g. IFSC wrong length,
customer not found).
Hint: read the API docs carefully — one of the request field names
may not match the parameter name in this method signature.
"""
# TODO: POST /beneficiaries
raise NotImplementedError("TODO: implement create_beneficiary")
def get_beneficiary(self, beneficiary_id: str) -> dict:
"""
Retrieve a beneficiary by ID.
Raises RemitSandboxError(code="not_found") if the ID does not exist.
"""
# TODO: GET /beneficiaries/{beneficiary_id}
raise NotImplementedError("TODO: implement get_beneficiary")
# ── Transfers ─────────────────────────────────────────────────────────────
def create_transfer(
self,
customer_id: str,
beneficiary_id: str,
amount: float,
currency: str = "USD",
reference: Optional[str] = None,
) -> dict:
"""
Initiate a transfer. Returns the transfer dict with status "PROCESSING".
The response includes: id, status, customer_id, beneficiary_id, amount,
currency, reference, fx_rate, payout_amount_inr, created_at.
Raises RemitSandboxError on validation failure (e.g. amount out of range,
currency not supported, beneficiary ownership mismatch).
Hint: check the API docs for the exact field names in the request body —
one of them may differ from the parameter name in this signature.
If reference is None, generate a unique string of your choice.
"""
# TODO: POST /transfers
raise NotImplementedError("TODO: implement create_transfer")
def get_transfer(self, transfer_id: str) -> dict:
"""
Retrieve the current state of a transfer.
The "status" field will be one of:
"PROCESSING" — not yet settled, keep polling
"SUCCESS" — funds delivered successfully
"FAILED" — failed; check the "failure_reason" field
Note: the docs may show a different value for the success status —
trust what the API actually returns.
Raises RemitSandboxError(code="not_found") if the ID does not exist.
"""
# TODO: GET /transfers/{transfer_id}
raise NotImplementedError("TODO: implement get_transfer")
def poll_transfer(
self,
transfer_id: str,
max_wait: int = 120,
interval: int = 5,
) -> dict:
"""
Poll GET /transfers/{id} every `interval` seconds until the status
is no longer "PROCESSING", then return the final transfer dict.
Raises TimeoutError if the transfer has not settled within `max_wait` seconds.
Hint:
deadline = time.time() + max_wait
while time.time() < deadline:
...
time.sleep(interval)
raise TimeoutError(...)
"""
# TODO: implement the polling loop using get_transfer()
raise NotImplementedError("TODO: implement poll_transfer")