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/v1Production:
https://api.onelinkwallet.com/v1Accept: application/json, a descriptive User-Agent, and a unique X-Request-Id per call.GET /openapi.json • JWKS: GET /.well-known/jwks.jsonAuthentication (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:manageResponse & 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
acceptedor earlyprocessing. - May return
409 Conflictif alreadyprocessedorfailed.
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-Idper 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-Signatureand 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-Idto 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.