One Link Wallet API
Secure, standards-based REST API for creating and tracking transfers. Highlights: OAuth 2.1 Signed responses Idempotency RFC 7807 errors Webhooks
Sandbox:
https://api.sandbox.onelinkwallet.com/v1
Production:
https://api.onelinkwallet.com/v1
Accept: application/json
, a descriptive User-Agent
, and a unique X-Request-Id
per call.GET /openapi.json
• JWKS: GET /.well-known/jwks.json
Authentication (OAuth 2.1 Client Credentials)
# Request token (RFC 6749/8252; OAuth 2.1 profile)
curl -u CLIENT_ID:CLIENT_SECRET \
-d 'grant_type=client_credentials&scope=transfers:create transfers:read webhooks:manage' \
https://api.sandbox.onelinkwallet.com/oauth2/token
# Use token
curl -H "Authorization: Bearer <access_token>" \
-H "X-Request-Id: $(uuidgen)" \
https://api.sandbox.onelinkwallet.com/v1/transfers/trf_123
transfers:create
, transfers:read
, webhooks:manage
Response & Webhook Signing
Every response and webhook includes a signature to guarantee integrity and defend against replay. Compute HMAC-SHA256 over ${timestamp}.${raw_body}
using your shared secret. JSON bodies are canonicalized (RFC 8785 JCS).
X-OLW-Timestamp: 1735668000
X-OLW-Signature: sha256=1e5c7f...
- Reject if timestamp skew > 300s, signature mismatch, or nonce replay.
- Rotate secrets routinely; store in KMS/HSM; maintain key IDs (
kid
). - Alternate: JWS (RS256) with our JWKS at
/.well-known/jwks.json
.
Rate Limits & Idempotency
We publish RateLimit-*
headers (RFC 9331). Use Idempotency-Key
(UUIDv4 or 256-bit token) on all POSTs that create state.
Idempotency-Key: 8c9f2c18-5c3a-4b0f-9db7-8a4f7e4a7332
- Duplicate key within 24h returns the original result (or 409 if first failed validation).
- Retry policy: exponential backoff for 502/503/504 and network timeouts. Do not retry 4xx.
- Headers:
RateLimit-Limit
,RateLimit-Remaining
,RateLimit-Reset
.
Error Model (RFC 7807)
{
"type": "https://docs.onelinkwallet.com/errors/validation",
"title": "Invalid request",
"status": 400,
"detail": "currency must be ISO 4217",
"instance": "req_01HW...Q2",
"errors": [{"path":"/amount/currency","code":"invalid_format"}]
}
Scenario | HTTP | Notes |
---|---|---|
Validation error | 400 | Problem+JSON body; field-level errors[] |
Unauthorized | 401 | Invalid/expired token |
Forbidden | 403 | Missing scope or mTLS required |
Not found | 404 | Unknown resource |
Rate limit | 429 | With Retry-After seconds |
Destination error | 502/503 | Upstream PSP/wallet issue |
Timeout | 504 | Retry with backoff |
Resources
Create Transfer
POST /v1/transfers
Authorization: Bearer <token>
Idempotency-Key: <uuid>
Content-Type: application/json
{
"amount": {"value": "2500", "currency": "EUR"},
"source": {"walletId": "wal_5m4b7azr", "reference": "INV-90351"},
"destination": {"walletId": "wal_dp2v6g0q"},
"metadata": {"orderId": "o_12345", "notes": "Priority"},
"callbackUrl": "https://merchant.example.com/webhooks/olw"
}
201 Created
{
"transferId": "trf_0kz2cd8g",
"status": "accepted",
"decision": {"riskScore": 18, "action": "proceed", "reasons": ["geovelocity_ok","kyc_match"]},
"createdAt": "2025-01-05T14:30:00Z"
}
Request details
Field | Type | Required | Description |
---|---|---|---|
amount.value | string | yes | Minor units (e.g., cents). No decimals; max 12 digits. |
amount.currency | string | yes | ISO 4217 (e.g., EUR, USD, SAR). |
source.walletId | string | yes | Wallet to debit (format wal_* ). |
source.reference | string | no | Free-form reference, max 64 chars. |
destination.walletId | string | yes | Wallet to credit. |
metadata.* | object | no | Key/value pairs (up to 20 keys; values ≤ 256 chars). |
callbackUrl | string | no | Override default webhook URL for this transfer. |
Get Transfer
GET /v1/transfers/{transferId}
{
"transferId": "trf_0kz2cd8g",
"status": "processed",
"processedAt": "2025-01-05T14:31:07Z",
"receipt": {"hash": "b7c1...", "ledgerRef": "L-9384021"},
"decision": { "riskScore": 18, "action": "proceed" },
"amount": {"value":"2500","currency":"EUR"},
"source": {"walletId":"wal_5m4b7azr"},
"destination": {"walletId":"wal_dp2v6g0q"},
"metadata": {"orderId":"o_12345"}
}
List Transfers
GET /v1/transfers?limit=50&cursor=eyJwYWdlIjoyfQ&status=processed&createdFrom=2025-01-01T00:00:00Z
Query | Type | Description |
---|---|---|
limit | int (1–200) | Default 50 |
cursor | string | Opaque token from previous page |
status | enum | accepted|processing|processed|failed|canceled |
walletId | string | Filter by source/destination wallet |
createdFrom/To | datetime | RFC 3339 UTC |
sort | enum | createdAt_desc (default) or createdAt_asc |
{
"data": [ /* transfers */ ],
"nextCursor": "eyJwYWdlIjozfQ"
}
Cancel Transfer
POST /v1/transfers/{transferId}/cancel
Idempotency-Key: <uuid>
{
"transferId": "trf_0kz2cd8g",
"status": "canceled",
"canceledAt": "2025-01-05T14:30:20Z"
}
- Only allowed from
accepted
or earlyprocessing
. - May return
409 Conflict
if alreadyprocessed
orfailed
.
Schemas & Validation
Money
Field | Type | Constraints |
---|---|---|
value | string | Minor units; 1–12 digits; no sign/decimals |
currency | string | ISO 4217 (uppercase) |
WalletRef
Field | Type | Constraints |
---|---|---|
walletId | string | Pattern ^wal_[a-z0-9]{8,32}$ |
reference | string | Max 64 chars; printable ASCII |
Transfer
Field | Type | Notes |
---|---|---|
transferId | string | Server-issued trf_* |
status | enum | accepted|processing|processed|failed|canceled |
decision | object | Risk score, action, reasons |
receipt | object | Ledger/settlement references |
createdAt/processedAt | datetime | RFC 3339 UTC |
Common validation rules
- Currency must match supported set for each tenant/region.
- Amount must be ≥ 1 minor unit and within tenant limits.
- Source ≠ Destination wallet.
- Metadata total size ≤ 4 KB.
Status Lifecycle
accepted
→ basic validation passed; queued for processing.processing
→ routing, risk checks, destination handshake.processed
→ committed; receipt available.failed
→ validation/routing error; seeerrors[]
.canceled
→ canceled before irreversible step.
Observability & Correlation
- Send a unique
X-Request-Id
per call; we echo it back and include it in webhooks. - W3C Trace Context supported via
traceparent
/tracestate
. - Structured JSON logs; tamper-evident archival of audit trails.
Transport, TLS & CORS
- TLS 1.3 preferred; 1.2 allowed with modern AEAD ciphers (GCM/ChaCha20-Poly1305).
- HSTS enabled in production; no plaintext redirects for API hosts.
- CORS: restricted allow-list; no wildcard origins; preflight required.
- SSRF defenses on callback/webhook URLs; only
https://
with resolvable public DNS.
Code Examples
Node (fetch)
import fetch from "node-fetch";
const BASE = "https://api.sandbox.onelinkwallet.com/v1";
async function createTransfer(token){
const res = await fetch(BASE + "/transfers", {
method:"POST",
headers:{
"Authorization": "Bearer " + token,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
"X-Request-Id": crypto.randomUUID()
},
body: JSON.stringify({
amount:{value:"2500",currency:"EUR"},
source:{walletId:"wal_5m4b7azr"},
destination:{walletId:"wal_dp2v6g0q"}
})
});
if(!res.ok) throw new Error(await res.text());
return res.json();
}
Python (requests)
import requests, uuid
BASE="https://api.sandbox.onelinkwallet.com/v1"
headers={"Authorization":"Bearer "+ACCESS_TOKEN,
"Content-Type":"application/json",
"Idempotency-Key":str(uuid.uuid4()),
"X-Request-Id":str(uuid.uuid4())}
payload={"amount":{"value":"2500","currency":"EUR"},
"source":{"walletId":"wal_5m4b7azr"},
"destination":{"walletId":"wal_dp2v6g0q"}}
r=requests.post(f"{BASE}/transfers", json=payload, headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Webhooks
Configure with POST /v1/webhooks
. Events: transfer.created
, transfer.processed
, transfer.failed
. Retries: 0s, 30s, 2m, 10m, 1h (up to 15 attempts). Non-2xx responses trigger retries; 4xx are not retried except 429 (with backoff).
POST /v1/webhooks
{"url":"https://merchant.example.com/webhooks/olw","events":["transfer.processed"]}
POST /webhooks/olw
X-OLW-Event: transfer.processed
X-OLW-Delivery: evt_89k0w1
X-OLW-Timestamp: 1735668000
X-OLW-Signature: sha256=1e5c7f...
{"transferId":"trf_0kz2cd8g","status":"processed","processedAt":"2025-01-05T14:31:07Z"}
- Verify
X-OLW-Signature
and timestamp (<= 300s skew) before acknowledging. - Support rotating secrets (key IDs) and maintain an event de-duplication store (at-least-once delivery).
- Optional mTLS: present client cert; we validate against your registered CA chain.
2xx
within 10s. Use a dead-letter queue and alerting for repeated failures.Health, Status & SLAs
GET /v1/health
— liveness; unauthenticated 200 if healthy.GET /v1/status
— readiness; authenticated; returns upstream dependency status.- Retry budgets & circuit breaker guidance: exponential backoff, jitter; cap retries to avoid thundering herds.
- Availability targets published on status page; maintenance windows announced in advance.
Environments & Headers
- Sandbox is open after onboarding; production requires credentials approval.
- Always send:
Accept: application/json
,User-Agent
,X-Request-Id
. - Observability: we forward your
X-Request-Id
to logs and webhooks.
Compliance & Privacy
- GDPR/UK GDPR ready; DPA available. Data minimization required.
- FIPS 140-3 crypto modules; keys in managed KMS/HSM; rotation policy enforced.
- Log hygiene: never log tokens, secrets, or full PII; mask sensitive values.
- Retention defaults: 180 days logs, 365 days transfer metadata (tenant configurable).
- Data residency: stored in-region as contractually agreed; cross-border transfers based on appropriate safeguards.