API reference
Base URL https://plaiduz-api-production.up.railway.app/v1. All requests and responses are JSON. Every non-2xx response includes a request_id.
Quickstart
- Get a client_id and secret from the dashboard.
- Call /link/token/create from your server to get a short-lived link_token.
- Open the Link widget with that token; the user connects their bank and you receive a public_token.
- Exchange it for a permanent access_token via /item/public_token/exchange.
- Call /accounts/balance/get, /transactions/get and /payment/initiate.
Authentication
Send your credentials as headers (recommended) or in the JSON body. The demo sandbox keys are shown below.
Silkroute-Client-Id: client_demo_5f3a9c
Silkroute-Secret: sandbox-secret-DEMO-do-not-use/v1/link/token/createCreates a short-lived token that initialises the Link widget for one end user.
Request
{
"user_ref": "user_42",
"products": ["transactions", "payments"],
"language": "uz"
}Response 200
{
"link_token": "link-sandbox-3f9a…",
"expiration": "2026-09-29T12:30:00.000Z"
}/v1/item/public_token/exchangeExchanges the one-time public_token returned by Link for a permanent access_token. Revealed exactly once. A public_token is valid for 30 minutes. If you created the link_token with code_challenge (PKCE, method S256), send the matching code_verifier.
Request
{ "public_token": "public-sandbox-…" }Response 200
{
"access_token": "access-sandbox-…",
"item_id": "item_01H…"
}/v1/accounts/balance/getReturns real-time balances for every open account on the Item. Amounts are in major units.
Request
{ "access_token": "access-sandbox-…" }Response 200
{
"accounts": [{
"account_id": "acc_01M…",
"name": "UZCARD Salary", "mask": "4415",
"balances": {
"current": 79802.37, "available": 69160.47,
"iso_currency_code": "UZS" }
}]
}/v1/transactions/getPaginated, enriched transactions. amount is positive when money leaves the account.
Request
{
"access_token": "…",
"start_date": "2026-08-01",
"end_date": "2026-09-29",
"options": { "count": 100, "offset": 0 }
}Response 200
{
"total_transactions": 78,
"transactions": [{
"transaction_id": "txn_…", "amount": 45000,
"pending": false, "date": "2026-09-20",
"merchant_name": "Korzinka",
"personal_finance_category": {
"primary": "FOOD_AND_DRINK" }
}]
}/v1/payment/initiateInitiates a direct A2A transfer. Requires an idempotency_key; settlement is confirmed asynchronously. The response carries the simulated platform fee: at most 0.5% (50 bps), 0.3% (30 bps) at monthly volume, capped per payment. Sandbox: nothing is charged.
Request
{
"access_token": "…",
"account_id": "acc_…",
"amount": 150000, "currency": "UZS",
"creditor": {
"name": "OOO Beneficiary",
"account_number": "20208000900123456789" },
"idempotency_key": "order-4821"
}Response 200
{
"payment_id": "pay_01M…",
"status": "executing",
"amount": 150000, "currency": "UZS",
"fee": { "amount": 750, "currency": "UZS",
"bps": 50, "tier": "standard", "simulated": true }
}/v1/identity/getAccount-holder names, e-mail, masked phone and city, fetched from the bank on every call. Silkroute stores none of it. Sandbox identities are synthetic.
Request
{ "access_token": "access-sandbox-…" }Response 200
{
"accounts": [{
"account_id": "acc_01M…", "mask": "4415",
"owners": [{
"names": ["Dilnoza Karimova"],
"phone_numbers": [{ "data": "+998 ** *** ** 42" }],
"addresses": [{ "data": { "city": "Samarkand", "country": "UZ" } }]
}]
}],
"data_source": "synthetic_sandbox"
}/v1/user/insights/summaryConsent centre (Authorization: Bearer user-…): the user’s cash flow per month and spending per category across their connected banks, in whole UZS.
Request
{ "period": "90d" }Response 200
{
"period": { "start": "2026-07-12", "end": "2026-10-09" },
"cashflow": {
"totals": { "inflow": 28500000, "outflow": 12900000, "net": 15600000 },
"savings_rate_pct": 55 },
"categories": [{ "category": "RENT_AND_UTILITIES",
"amount": 7900000, "share_pct": 61 }]
}/v1/user/assistant/askConsent centre: a read-only assistant. It can only call three fixed tools (cash flow, category spend, risk score) for this user; every figure in the answer is in facts. Without an LLM key the answer is calculated, not generated. Limited per user per day.
Request
{
"question": "Where did my money go?",
"language": "en"
}Response 200
{
"answer": "From 2026-07-12 to 2026-10-09 you received 28 500 000 UZS …",
"mode": "llm",
"sources": [{ "tool": "get_cashflow", "period": { … } }],
"facts": [ … ],
"usage": { "questions_today": 1, "daily_limit": 10 }
}Platform products
Variable Recurring Payments (VRP)
Demo dataCreate a mandate on the user’s account, wait for the user to approve it (consent centre or Telegram), then charge variable amounts within the limits. Above-limit or inactive-mandate attempts come back with status "rejected" and a rejection_code — HTTP 200, not an error — and are still logged.
/v1/vrp/mandate/createRequests a mandate (status awaiting_authorization) and notifies the user. Limits are in major units; period windows follow the Asia/Tashkent calendar; expiry ≤ 3 years.
Request
{
"access_token": "access-sandbox-…",
"account_id": "acc_01J…",
"payee": { "name": "Shahar Kommunal",
"account_number": "20208000900765432109" },
"limits": { "max_per_payment": 500000,
"period": "month",
"period_limit": 1000000 },
"expires_at": "2027-10-01T00:00:00Z"
}Response 200
{
"mandate": {
"mandate_id": "mandate_01J…",
"status": "awaiting_authorization",
"payee": { "name": "Shahar Kommunal",
"account_mask": "••••2109" },
"limits": { "max_per_payment": 500000,
"period": "month", "period_limit": 1000000 },
"usage": { "used": 0, "remaining": 1000000 }
}
}/v1/vrp/payment/createCharges a variable amount under an active mandate. idempotency_key is required. Accepted charges settle asynchronously as A2A payments.
Request
{
"mandate_id": "mandate_01J…",
"amount": 645000,
"idempotency_key": "bill-2026-10-heating"
}Response 200
{
"charge": {
"charge_id": "vrpc_01J…",
"status": "rejected",
"rejection_code": "LIMIT_PER_PAYMENT_EXCEEDED",
"amount": 645000, "payment_id": null
},
"remaining_in_period": 399700
}More endpoints
| POST | |
|---|---|
/v1/vrp/mandate/get | Status, limits and current-period usage. |
/v1/vrp/mandate/list | Filter by status or user_ref. |
/v1/vrp/mandate/revoke | Cancel as the payee (revoked_by = client). |
/v1/vrp/charges/list | Every attempt, including rejected ones. |
Consent centre (user_token)
Mint a user_token on your server (POST /v1/user/token/create) and call these on the user’s behalf — or the user taps Approve / Pause / Revoke in Telegram. Keep the user_token server-side.
| POST · Bearer user_token | |
|---|---|
/v1/user/vrp/mandates/list | The user’s mandates. |
/v1/user/vrp/mandate/authorize | Approve with SCA (sandbox OTP 123456). |
/v1/user/vrp/mandate/pause | Charges are refused until resumed. |
/v1/user/vrp/mandate/resume | Resume a paused mandate. |
/v1/user/vrp/mandate/revoke | Revoke permanently — effective on the next charge. |
/v1/user/vrp/charges/list | Charges under one of the user’s mandates. |
Webhooks
VRP_MANDATE_STATUS_UPDATEDawaiting_authorization → active / paused / revoked / expired / declined.
Webhooks
VRP_PAYMENT_STATUS_UPDATEDA charge was accepted, settled or rejected (with rejection_code).
Error codes
| error_code | |
|---|---|
INVALID_MANDATE_LIMITS | period_limit < max_per_payment, expiry in the past or more than 3 years ahead (400). |
MANDATE_STATE_CONFLICT | That transition is not allowed from the current status (409). |
MANDATE_NOT_FOUND | Unknown mandate, or not yours (404). |
rejection_code | LIMIT_PER_PAYMENT_EXCEEDED · PERIOD_LIMIT_EXCEEDED · PERIOD_COUNT_EXCEEDED · MANDATE_NOT_AUTHORIZED · MANDATE_PAUSED · MANDATE_REVOKED · MANDATE_EXPIRED · INSUFFICIENT_FUNDS · ITEM_REVOKED |
Credit Passport
Demo dataAsk the borrower for consent, wait until they grant it, then fetch a 300–850 cash-flow score with five explained factors. The consent_token is returned once and works only while access is granted; every fetch is logged and shown to the borrower.
- Factors: income_stability 0.30 · inflow_regularity 0.15 · spending_ratio 0.20 · balance_resilience 0.15 · debt_burden 0.20. score = 300 + 550 × Σ(weight × sub_score) / 100.
- Flags: HIGH_DEBT_BURDEN (debt service ≥ 50% of income, score capped at 599), INSUFFICIENT_HISTORY (< 3 full months), NO_INCOME_DETECTED.
/v1/credit_passport/consent/requestCreates a pending consent and notifies the borrower. period_months: 3, 6 or 12; access_ttl_days: 1–90 (default 30). An unanswered request lapses after 7 days. The user must have at least one connected bank.
Request
{
"user_ref": "borrower_4821",
"purpose": "Consumer loan application #4821",
"period_months": 6,
"access_ttl_days": 30
}Response 200
{
"consent_id": "pcon_01J…",
"consent_token": "consent-sandbox-…",
"status": "pending",
"requester_name": "Acme Microfinance",
"request_expires_at": "2026-10-10T10:00:00Z"
}/v1/credit_passport/getReturns the passport while consent is granted. A report is reused for 24 hours; each call is still logged and notified.
Request
{
"consent_token": "consent-sandbox-…"
}Response 200
{
"passport": {
"score": 712, "grade": "B", "confidence": "high",
"model_version": "cf-v1", "flags": [],
"factors": [{
"key": "debt_burden", "weight": 0.2,
"sub_score": 100, "impact": "positive",
"explanation": "No loan payments detected."
}, …],
"metrics": { "months_observed": 6,
"avg_monthly_income": 9500000,
"debt_to_income": 0 },
"data_source": "synthetic_sandbox"
}
}More endpoints
| POST | |
|---|---|
/v1/credit_passport/consent/get | Status, access window and fetch count. |
/v1/credit_passport/consent/list | Filter by status or user_ref; includes the latest score. |
/v1/credit_passport/consent/revoke | End your own access early (revoked_by = client). |
Consent centre (user_token)
The borrower sees who asked, why, and every access — and decides. In Telegram they get Grant / Deny on a request and Revoke on every access.
| POST · Bearer user_token | |
|---|---|
/v1/user/passport/consents/list | Requests with their full access log. |
/v1/user/passport/consent/grant | Grant with SCA (sandbox OTP 123456). |
/v1/user/passport/consent/deny | Refuse a pending request. |
/v1/user/passport/consent/revoke | Revoke instantly — the next fetch gets 403. |
/v1/user/passport/preview | My own score and explanation (not shared, not logged as access). |
/v1/user/items/remove | Disconnect a bank: its data is no longer analysed. |
Webhooks
CREDIT_PASSPORT_CONSENT_UPDATEDpending → granted / denied / revoked / expired.
Error codes
| error_code | |
|---|---|
CONSENT_PENDING · CONSENT_DENIED · CONSENT_REVOKED · CONSENT_EXPIRED | No active consent for this token (403). |
NO_CONNECTED_ACCOUNTS | The user has no connected bank to analyse (400). |
INVALID_CONSENT_TOKEN | Unknown consent_token (400). |
CONSENT_STATE_CONFLICT | That decision is no longer possible (409). |
Auto-Balancing
Demo dataRules belong to the user, so they are created with a user_token from your server (or in the consent centre). The engine projects the protected account from scheduled debits and detected recurring outflows; before a shortfall it suggests a top-up (notify) or transfers immediately (auto). amount = min(shortfall, max_transfer_amount, donor balance above source_min_balance).
- Sandbox: a rule name containing FLAKY makes the first transfer attempt fail once (retry demo); FAIL makes every attempt fail.
/v1/user/balancing/rules/createCreates the rule and evaluates it at once. horizon_days 1–60 (default 7) or a deadline_date; both accounts must be the user’s own, in one currency.
Request
{
"name": "Keep my card topped up",
"target_account_id": "acc_card…",
"source_account_id": "acc_savings…",
"horizon_days": 7,
"max_transfer_amount": 2000000,
"source_min_balance": 5000000,
"mode": "notify"
}Response 200
{
"rule": { "rule_id": "abr_01J…", "status": "active", "mode": "notify" },
"evaluation": {
"evaluation": { "action": "transfer", "amount": 900000,
"shortfall_date": "2026-10-17",
"projected_min_balance": -899877 },
"execution": { "execution_id": "abx_01J…", "status": "suggested" }
}
}/v1/user/balancing/executions/confirmConfirms a suggestion exactly once (the same as the Telegram Confirm button). A second confirm returns 409.
Request
{
"execution_id": "abx_01J…"
}Response 200
{
"execution": {
"execution_id": "abx_01J…", "status": "executing",
"amount": 900000, "decided_by": "user",
"attempts": 1, "max_attempts": 3
},
"result": "transfer_confirmed"
}More endpoints
| POST | |
|---|---|
/v1/balancing/rules/list | Your users’ rules (filter by user_ref / status). |
/v1/balancing/executions/list | Every decision and transfer attempt. |
/v1/balancing/rules/pause | Support action: pause a user’s rule (audited). |
/v1/sandbox/upcoming_debit/create | Sandbox: schedule a debit (standing order) to trigger a forecast. |
The user’s rules (user_token)
Users see and control every rule — in your app, the consent centre or the Mini App — and get Confirm / Dismiss in Telegram.
| POST · Bearer user_token | |
|---|---|
/v1/user/balancing/rules/list | Rules with their latest projection. |
/v1/user/balancing/rules/update | Change limits, horizon, deadline or mode. |
/v1/user/balancing/rules/pause | Pause — open suggestions expire. |
/v1/user/balancing/rules/resume | Resume a paused rule. |
/v1/user/balancing/rules/delete | Delete a rule. |
/v1/user/balancing/rules/evaluate | “Check now”; dry_run=true only projects. |
/v1/user/balancing/executions/list | The decision log with each attempt. |
/v1/user/balancing/executions/confirm | Confirm a suggested top-up. |
/v1/user/balancing/executions/dismiss | Dismiss a suggested top-up. |
Webhooks
BALANCING_TRANSFER_UPDATEDsuggested → executing → executed / failed; dismissed; expired.
Error codes
| error_code | |
|---|---|
SAME_ACCOUNT · CURRENCY_MISMATCH · INVALID_DEADLINE | Invalid rule (400). |
ACCOUNT_NOT_FOUND | An account is not one of the user’s own connected accounts (404). |
EXECUTION_STATE_CONFLICT | The suggestion is no longer open (409). |
skip_reason / last_error | SOURCE_BELOW_FLOOR · TECHNICAL_ERROR (retried) · INSUFFICIENT_FUNDS · ITEM_REVOKED |
Treasury & Split Payments
Demo dataB2B only: every call acts on behalf of a treasury member of your company — send actor_ref in the body or the Silkroute-Actor header. Roles: director (everything) and accountant (view, preview and initiate payments). The API-key holder can bootstrap the first director; the last active director cannot be demoted or disabled (409 LAST_DIRECTOR). All endpoints are POST under /v1/treasury. A split pays one counterparty from several accounts: each part is a separate A2A bank payment, and the overall status is always derived from the parts.
- Sandbox: sandbox_simulate: { fail_part: <part_no>, failure: "transient" | "permanent", refuse_return: true } on /split/create (sandbox keys only; otherwise SANDBOX_ONLY) lets you watch retries, re-allocation and compensation.
- Execution: each part is a separate A2A payment, retried on a technical bank error (up to 3 attempts per part). If a part fails for good, auto mode re-allocates the missing amount once to other accounts; otherwise already-settled parts are compensated (the counterparty returns them).
- Sub-treasury: the optional account_ids on /balances, /split/preview, /split/create and /split/list limits the call to a subset of the company’s treasury accounts (for example one branch or one currency pool). Auto allocation and re-allocation only use those accounts; manual shares outside the scope are rejected with UNKNOWN_ACCOUNT; balances and lists only cover splits funded from those accounts. Omit it to use the whole organisation’s treasury (default).
/v1/treasury/split/createPays one counterparty {name, account_number} the total_amount (major units, UZS). allocation {mode:"auto"} fills accounts by priority while respecting min_reserve; {mode:"manual", shares:[…]} takes up to 10 shares, each {account_id, amount} or {account_id, percent}. idempotency_key is required — the same key replays the result and nothing is paid twice. Optional reference and account_ids (sub-treasury scope).
Request
{
"actor_ref": "accountant",
"counterparty": { "name": "OOO Paxta Trade",
"account_number": "2020…4321" },
"total_amount": 50000000,
"allocation": { "mode": "auto" },
"account_ids": ["acc_01J…", "acc_01J…"],
"idempotency_key": "inv-0418-paxta",
"reference": "Invoice 0418"
}Response 200
{
"split": {
"split_id": "split_01J…",
"status": "completed",
"counterparty": { "name": "OOO Paxta Trade", "account_mask": "••••4321" },
"total_amount": 50000000,
"currency": "UZS",
"allocation_mode": "auto",
"created_by": { "member_ref": "accountant",
"display_name": "Bekzod (Accountant)",
"role": "accountant" },
"parts": [
{ "part_no": 1, "account_id": "acc_01J…",
"account_label": "Tumaris · Settlement ••1111",
"amount": 35000000, "status": "settled",
"attempts": 1, "payment_id": "pay_01J…" },
{ "part_no": 2, "account_id": "acc_01J…",
"account_label": "Lochin · Settlement ••2222",
"amount": 15000000, "status": "settled",
"attempts": 1, "payment_id": "pay_01J…" }
],
"progress": { "settled": 50000000, "in_flight": 0,
"failed": 0, "compensated": 0 }
}
}/v1/treasury/split/getReturns one split with its parts, attempts and progress.
Request
{
"actor_ref": "accountant",
"split_id": "split_01J…"
}Response 200
{
"split": {
"split_id": "split_01J…",
"status": "completed",
"counterparty": { "name": "OOO Paxta Trade", "account_mask": "••••4321" },
"total_amount": 50000000,
"currency": "UZS",
"allocation_mode": "auto",
"created_by": { "member_ref": "accountant",
"display_name": "Bekzod (Accountant)",
"role": "accountant" },
"parts": [
{ "part_no": 1, "account_id": "acc_01J…",
"account_label": "Tumaris · Settlement ••1111",
"amount": 35000000, "status": "settled",
"attempts": 1, "payment_id": "pay_01J…" },
{ "part_no": 2, "account_id": "acc_01J…",
"account_label": "Lochin · Settlement ••2222",
"amount": 15000000, "status": "settled",
"attempts": 1, "payment_id": "pay_01J…" }
],
"progress": { "settled": 50000000, "in_flight": 0,
"failed": 0, "compensated": 0 }
}
}More endpoints
| POST | |
|---|---|
/v1/treasury/balances | Unified balances: per currency {current, available, reserved, spendable}, per bank and per account (with min_reserve and spendable), 30-day inflow / outflow and splits_in_flight. Optional account_ids. |
/v1/treasury/split/preview | Shows the allocation for a split; nothing moves. Optional account_ids. |
/v1/treasury/split/list | Splits, with optional status and limit (≤ 200) and the account_ids scope. |
/v1/treasury/split/retry_compensation | Director only: retry a counterparty return that was refused. |
/v1/treasury/members/list | Treasury members and their roles. |
/v1/treasury/members/add | Adds a member as director or accountant. The API-key holder can bootstrap the first director. |
/v1/treasury/members/update | Changes a role or disables a member; the last active director is protected (409 LAST_DIRECTOR). |
/v1/treasury/accounts/add | Director only: connects an account with label, priority (1–1000, default 100; lower is used first in auto allocation) and min_reserve (never spent below it). |
/v1/treasury/accounts/remove | Director only: removes a treasury account. |
/v1/treasury/audit/list | Director only: every member / account / split action with the acting member. |
Webhooks
SPLIT_PAYMENT_STATUS_UPDATED{split_id, new_status, total_amount} on every status change.
Error codes
| error_code | |
|---|---|
NO_TREASURY_ACCOUNTS · INSUFFICIENT_TREASURY_FUNDS | There are no treasury accounts in scope, or their spendable funds do not cover the split (TREASURY_ERROR). |
UNKNOWN_ACCOUNT · DUPLICATE_ACCOUNT · CURRENCY_MISMATCH | A share names an account that is not in your treasury or is outside the account_ids scope; an account is added twice; currencies differ (TREASURY_ERROR). |
MIXED_SHARES · SHARES_DO_NOT_SUM · INVALID_SHARE · TOO_MANY_PARTS | Invalid manual allocation: amount and percent mixed, shares that do not add up to the total, a malformed share, or more than 10 parts (TREASURY_ERROR). |
EXCEEDS_HEADROOM | A share is more than the account can spend above its min_reserve (TREASURY_ERROR). |
NOTHING_TO_RETRY | There is no refused counterparty return to retry (TREASURY_ERROR). |
ACTOR_REQUIRED · NOT_A_TREASURY_MEMBER · MEMBER_DISABLED · INSUFFICIENT_ROLE | The actor is missing, unknown, disabled, or lacks the role for this call (INVALID_REQUEST). |
MEMBER_EXISTS · LAST_DIRECTOR | The member already exists (INVALID_REQUEST); the last active director cannot be demoted or disabled (409). |
SANDBOX_ONLY | sandbox_simulate was sent with a non-sandbox key (INVALID_REQUEST). |
split.status | processing · completed · partially_failed · compensating · compensated · compensation_failed · failed |
parts[].status | pending · executing · settled · failed · rejected · replaced · compensating · compensated · compensation_failed · cancelled |
White-label & platform
Demo dataFor banks, MFIs, fintechs and marketplaces that ship open banking under their own brand — Open Banking as a Service. Most of it lives in the dashboard: branding, API keys, rate limits, usage analytics and webhooks are managed per organisation, and every change is audited. The API itself is two POST calls: /v1/link/token/create (with an optional sandbox branding override) and /v1/link/branding, which the Link widget calls with its link_token. “Secured by Silkroute” always stays visible on the consent flow.
- Branding: saved per organisation in the dashboard (Branding page; owners, admins and developers) and applied to every Link consent flow. “Preview in Link” shows an unsaved look in a sandbox session and saves nothing. “Secured by Silkroute” always stays visible.
- API keys: created in the dashboard — the secret is shown exactly once and only its hash is stored. Rotate (the old secret stops immediately) or revoke. Keys are sandbox or production; production keys can be created but stay inert in this sandbox build (403 PRODUCTION_NOT_ENABLED). The shared public demo key cannot be rotated, re-limited or revoked (409 PROTECTED_KEY).
- Rate limits and usage: each key has a limit (default 600 a minute, 1–100000). Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; over the limit the call fails with 429 RATE_LIMIT_EXCEEDED and a Retry-After header. The dashboard shows requests per day (ok / client errors / throttled / server errors), error rate, p50 / p95 latency per endpoint, requests and throttles per key, and request logs you can filter by key.
- Webhooks: endpoints belong to your organisation, and each delivery is signed with Silkroute-Signature: sha256=<HMAC-SHA256 of the raw body with your webhook secret> — check it with verifyWebhook from the SDK. A failed delivery is retried with exponential backoff (2^n minutes, capped at 6 h) and then marked dead. In the dashboard, enable or disable endpoints, redeliver an event and send a test event. Every platform change lands in the audit log.
/v1/link/token/createCreates the link_token for one end user (Silkroute-Client-Id / Silkroute-Secret); user_ref is required. The optional branding object — display_name (1–40 characters, no markup), primary_color and accent_color (#RRGGBB), logo_url, support_url and privacy_url (https only, up to 500 characters, no credentials), consent_headline (up to 120) — brands just that one session; fields you leave out fall back to your saved branding, or to the defaults. It is sandbox-only: any other key gets 400 SANDBOX_ONLY, and invalid values get 400 INVALID_BRANDING. Production sessions always use the branding saved in the dashboard.
Request
{
"user_ref": "user_42",
"products": ["transactions"],
"branding": {
"display_name": "NovaPay",
"primary_color": "#7C3AED",
"accent_color": "#0B0F19",
"logo_url": "https://cdn.novapay.uz/logo.svg",
"support_url": "https://novapay.uz/help",
"privacy_url": "https://novapay.uz/privacy",
"consent_headline": "Connect your bank to NovaPay"
}
}Response 200
{
"link_token": "link-sandbox-3f9a…",
"expiration": "2026-10-03T10:30:00.000Z"
}/v1/link/brandingCalled by the Link widget and authenticated by the link_token. Returns the branding to render: on_primary_color is the readable text colour picked by WCAG contrast, secured_by is always "Silkroute", and source tells where the look came from — organization (saved in the dashboard), preview (a sandbox override on this session) or default (nothing saved yet).
Request
{
"link_token": "link-sandbox-3f9a…"
}Response 200
{
"branding": {
"display_name": "NovaPay",
"logo_url": "https://cdn.novapay.uz/logo.svg",
"primary_color": "#7C3AED",
"on_primary_color": "#FFFFFF",
"accent_color": "#0B0F19",
"support_url": "https://novapay.uz/help",
"privacy_url": "https://novapay.uz/privacy",
"consent_headline": "Connect your bank to NovaPay",
"secured_by": "Silkroute",
"source": "preview"
}
}Webhooks
TRANSACTIONS_SYNCEDNew transactions are available for an Item.
Webhooks
ITEM_LOGIN_REQUIREDThe user must re-authenticate the Item.
Webhooks
ITEM_ERRORAn Item entered an error state.
Webhooks
PAYMENT_STATUS_UPDATEDAn A2A payment changed status (executing, settled, failed…).
Webhooks
DEFAULT_UPDATETest / generic update events — the dashboard’s test event arrives as DEFAULT_UPDATE.
Webhooks
VRP_MANDATE_STATUS_UPDATEDA VRP mandate was approved, paused, resumed, revoked or expired.
Webhooks
VRP_PAYMENT_STATUS_UPDATEDA charge under a VRP mandate was accepted, settled or rejected.
Webhooks
CREDIT_PASSPORT_CONSENT_UPDATEDA Credit Passport consent was granted, denied, revoked or expired.
Webhooks
BALANCING_TRANSFER_UPDATEDAn auto-balancing top-up was suggested, executed, skipped or failed.
Webhooks
SPLIT_PAYMENT_STATUS_UPDATEDA split payment changed status (completed, compensated…).
Error codes
| error_code | |
|---|---|
INVALID_BRANDING | A branding value is invalid: display_name over 40 characters or with markup; a colour that is not #RRGGBB; a link that is not https, longer than 500 characters or with credentials; or consent_headline over 120 characters (400, INVALID_INPUT). |
SANDBOX_ONLY | A branding override was sent on /v1/link/token/create with a non-sandbox key (400, INVALID_REQUEST). |
RATE_LIMIT_EXCEEDED | The key went over its per-minute limit; wait the number of seconds in the Retry-After header (429, RATE_LIMIT_EXCEEDED). |
PRODUCTION_NOT_ENABLED | A production key was used; production keys can be created but stay inert in this sandbox build (403, INVALID_REQUEST). |
INVALID_WEBHOOK_EVENT | An unknown event type was named — use one of the 10 webhook events (400, INVALID_INPUT). |
PROTECTED_KEY | Dashboard only: the shared public demo key cannot be rotated, re-limited or revoked (409, INVALID_REQUEST). |
INSUFFICIENT_ROLE | Dashboard only: your dashboard role cannot change platform settings (403, INVALID_REQUEST). |
branding.source | organization · preview · default |
Webhooks
We POST signed events to your endpoint. Verify Silkroute-Signature: sha256=… (HMAC-SHA256 over the raw body). Failed deliveries retry with backoff.
TRANSACTIONS_SYNCEDNew transactions are available for an Item.
ITEM_LOGIN_REQUIREDThe user must re-authenticate the Item.
PAYMENT_STATUS_UPDATEDA payment changed state (executing → settled/failed).
Sandbox credentials
| Field | Value |
|---|---|
| client_id | client_demo_5f3a9c |
| secret | sandbox-secret-DEMO-do-not-use |
| user_ref | enduser_demo_001 |
| Sandbox banks | ins_tumaris, ins_lochin, ins_bulut (fictional) |
| Any bank login | works except user_bad |
| OTP code | 123456 |
| Force rejection | creditor account ending 0000 |
Error codes
Errors use the shape { error_type, error_code, error_message, display_message, request_id }.
| error_code | |
|---|---|
INVALID_CODE_VERIFIER | PKCE: code_verifier missing or does not match the link_token’s code_challenge. |
INVALID_LOGIN_SESSION | The OTP step came from another Link session or the login expired. |
RATE_LIMIT_EXCEEDED | Too many requests; retry after the Retry-After header. |
ASSISTANT_DAILY_LIMIT | The user has used today’s assistant questions. |
INVALID_API_KEYS | client_id or secret is wrong (401). |
INVALID_LINK_TOKEN | link_token is invalid, expired or used. |
INVALID_ACCESS_TOKEN | access_token does not resolve to an Item. |
ITEM_LOGIN_REQUIRED | The Item needs re-authentication. |
INVALID_MFA | The one-time code was incorrect. |
INSUFFICIENT_FUNDS | Debtor account cannot cover the payment. |
CURRENCY_MISMATCH | Payment currency ≠ account currency. |