| Environment | Endpoint | Best for |
|---|---|---|
| UAT | https://mcp.uat.sokin.com/mcp | Building and testing an integration. This is the environment this reference documents. |
| Production | https://mcp.sokin.com/mcp | Live production data and real corporate accounts. Arranged through your Sokin contact. |
| Field | Value |
|---|---|
| Transport | Streamable HTTP |
| Protocol | JSON-RPC 2.0 (MCP) |
| Authentication | OAuth 2.1 + PKCE, with Dynamic Client Registration |
| Tools | 26 |
| Session model | Stateless: every request is independent |
tools/callinitialize round trip.text/event-stream with the result in a data: event, not as aAccept header must include both content types. SendAccept: application/json, text/event-stream. Omitting the second value| Endpoint | Purpose |
|---|---|
/.well-known/oauth-authorization-server | Advertises the server you are talking to as the issuer, plus its authorize and register endpoints |
/.well-known/oauth-protected-resource | Points back at that same server as the authorization server for this resource |
/register, so clientsopenid, profile, email.offline_access yet, so there is no refresh token. Aoffline_access -- so today it never fires.)/mcp are additionally gated by Host header checks (DNS421, not 401. If you see a 421,Host header before looking at your token.CallToolResult with three parts:| Part | Type | What it carries |
|---|---|---|
content | array of text blocks | The model-facing rendering: a sentence or a markdown table. Always present, and always compact in every mode. |
structuredContent | object, or absent | The machine-readable payload. This is the half the full-response opt-in widens. See the caveats below; it is not present on every result. |
isError | boolean | Whether the call failed. |
content is what a model reads cheaply, structuredContent isstructuredContent is absent in two cases, and code that parses resultscontent and isError: true andisError before reaching into structuredContent, orKeyErrorplay_cheetah_dash, which returns a bare string and never goesstructuredContent on success.message fieldmessage field, filled by the B2B API's ownmessage or "Success.". It is almost always thatstructuredContent; some pass it| Behaviour | Tools |
|---|---|
| Stripped | list_corporates, list_beneficiaries, get_beneficiary, get_beneficiary_schema, validate_beneficiary, create_beneficiary, update_beneficiary, list_financial_institution_branches, validate_payment_instruction, settle_fx_quote |
| Passed through | list_accounts, get_account, get_account_ledger, list_instructions, get_instruction, get_instruction_request, list_financial_institutions |
| Not applicable, no upstream envelope | get_fx_quote, create_fx_quote, create_payment_instruction, create_fx_payment_instruction, get_documentation_links, visualize, play_cheetah_dash |
| Both, depending on the branch taken | submit_payment_instruction, submit_fx_payment_instruction: stripped on a fresh submission, passed through on a replay that reports an existing payment's state |
structuredContent is compact: trimmed to the fields a_meta side channel.{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "list_corporates",
"arguments": {},
"_meta": { "sokin.com/full-response": true }
}
}true. A"true" is ignored. This fails closed on purpose, so a proxytrue; any other non-empty value is ignored. This path exists because_meta on an individual call, even though the MCP SDK beneath themlist_corporates call's structuredContent, compact by{
"data": {
"items": [
{
"corporateReference": "CORP-1",
"corporateName": "Acme Ltd",
"assignmentType": "direct"
}
]
}
}{
"data": {
"items": [
{
"corporateReference": "CORP-1",
"corporateName": "Acme Ltd",
"assignmentType": "direct",
"activeFrom": "2026-01-01T00:00:00Z",
"features": { "paymentAcceptance": true }
}
]
}
}content is identical either way: only structuredContent widens.structuredContent itself reaches theisError: true and, with one narrowstructuredContent at all: only contentisError before parsing. Never string-match the| Class | Message shape | Cause |
|---|---|---|
| Reference not available (403/404) | That reference is not available on this account. Check it against the value the corresponding list tool returned, and retry. If the reference is correct, this account may not have access to it. | The reference does not exist, is malformed, belongs to another corporate, or this account lacks permission for it. All four render identically and carry no status code, deliberately: telling them apart would answer "does this reference exist elsewhere in Sokin". The middle sentence names the tool's own reference arguments and the list tool that produces each — every tool taking a reference declares one (sokin_tool(reference_hint=...)), enforced by test_every_tool_taking_a_reference_names_it. Only list_corporates and list_financial_institutions, which take no reference, show the generic middle sentence. |
| B2B API error (any other status) | API error (400): <message from the API> | The upstream Sokin API rejected the call for a stated, non-reference reason. The status and message are passed through as is — except that a message carrying the identity-provider tenant name, an internal principal id or a permission string is replaced with no further detail available. |
| Unexpected failure | Something went wrong while processing that request. Please try again, and contact support if it keeps happening. | Anything unanticipated: a malformed upstream response, a bug. Internal detail is deliberately not leaked. |
| Schema validation | Rejection from the MCP layer, before the tool body ever runs | A wrongly typed argument, or a value outside a closed Literal enum. |
| Tool-local validation | Usually Error: <specific problem>, but not reliably; see below | Per-tool argument checks: blank references, unsupported pagination parameters, malformed amounts. Listed per tool below. |
Error: , but two do not:visualize returns markdown must not be empty. andlist_financial_institution_branches returns Unknown financial institution
id: '999'. Call list_financial_institutions to see valid ids. ThatisError: noError: is not on its own proof that isError iscreate_payment_instruction and create_fx_payment_instruction cancontent text that begins with the literal word Error: (anisError stays false -- these are reported findings from the validation502 through the same channel rather than rendered as a falsevalidate_payment_instruction does this when a 200 arrives with202 arrives with noexternalReference.{
"data": [ "..." ],
"pagination": {
"nextToken": "eyJrIjoi...",
"previousToken": null,
"totalItems": 128,
"hasMore": true
}
}hasMore is true, pass nextToken back as the next_token argument.nextToken is the field the API returns, and next_token is the parameterprevious_token parameter, so previousToken is| Tool | Pagination |
|---|---|
get_account_ledger | Cursor: limit, next_token |
list_instructions | Cursor: limit, next_token |
list_beneficiaries | Cursor: limit, next_token |
list_financial_institution_branches | Cursor: limit, next_token |
list_accounts | Envelope present but inert: the upstream route hardcodes hasMore: false and accepts no pagination parameter at all, so this always returns everything in one call |
list_corporates | None |
list_financial_institutions | None; the upstream endpoint accepts no parameters at all |
readOnlyHint: true) and move no money.| Tool | Annotations | What it does |
|---|---|---|
create_fx_quote | readOnlyHint: false, destructiveHint: false, idempotentHint: false | Mints a real, short-lived FX quote. Additive: it only ever creates, never mutates or removes, and it moves no money on its own |
settle_fx_quote | readOnlyHint: false, destructiveHint: true, idempotentHint: true | Executes a real FX conversion between two of the corporate's own currency accounts |
submit_payment_instruction | readOnlyHint: false, destructiveHint: true, idempotentHint: true | Submits a prepared payment to a beneficiary |
submit_fx_payment_instruction | readOnlyHint: false, destructiveHint: true, idempotentHint: true | Submits a prepared FX payment: converts and pays a beneficiary in one motion against a live quote |
validate_beneficiary | readOnlyHint: false, destructiveHint: false, idempotentHint: true | Verifies payee details; its existing-payee mode persists refreshed validation statuses (convergent) |
create_beneficiary | readOnlyHint: false, destructiveHint: false, idempotentHint: false | Creates a payee behind an emailed one-time code; a repeat call creates a second payee (no upstream dedupe) |
update_beneficiary | readOnlyHint: false, destructiveHint: true, idempotentHint: true | Overwrites a payee's destination/identity details behind the same emailed code |
validate_payment_instructioncreate_payment_instruction andcreate_fx_payment_instruction prepare a draft for a human to look atcreate_fx_payment_instruction'screate_beneficiary).create_payment_instruction/submit_payment_instruction andcreate_fx_payment_instruction/submit_fx_payment_instruction used toinitialize time. That guard was removed from all four toolssettle_fx_quote's "what thecreate -> confirm in chat -> submit flow for a hostcreate_fx_quote and settle_fx_quote keep a hard capability refusalserver.py's own UI_ONLY_TOOL_NAMES set and settle_fx_quote's entrysettle_fx_quote).create_* tool renders its review card and itssubmit_* tool.create_* tool's own content text spells theidempotencyKey out in plain words -- structuredContent is not reliablysubmit_* tool itself with that key andstructuredContent does not change between these two branches: idempotencyKeycontent wordingsubmit_* tools accept a direct call under eithervalidate_payment_instruction and create_payment_instruction both persistsubmit_payment_instructioncreate_fx_payment_instruction and submit_fx_payment_instruction follow acreate_fx_payment_instruction no longer takes a quote_id.buy_currency, sell_currency, amount, fixed_side,corporate_reference) and mints its own quote internally, via the same FXcreate_fx_quote uses, rather than requiring a quote mintedsubmit_fx_payment_instruction still takesquote_id as a parameter: it has to match that same draft. See bothcreate_fx_quote and settle_fx_quote exist only to back that card'ssettle_fx_quote's entry for why, and for whatlist_corporatescorporateReference, and this is where one comes from. Takes no arguments.content, one sentence:2 corporates: CORP-8842 — Northwind Trading Ltd (direct assignment); CORP-9110 — Northwind Logistics BV (inherited access).structuredContent, envelope {"data": {"items": [...]}}, each item:| Field | Type | Description |
|---|---|---|
corporateReference | string | The reference to pass to every other tool |
corporateName | string | Display name |
assignmentType | string | direct or inherited |
activeFrom (an ISO-8601 timestamp nothing in thisfeatures (an opaque corporate feature-flag objectmessage field, which the compact response strips.list_accounts| Parameter | Type | Required | Description |
|---|---|---|---|
corporate_reference | string | yes | The corporateReference from list_corporates |
content, one sentence:2 accounts for CORP-8842: CCA-1001 (GBP) — balance 48200.00, of which 47150.00 available; CCA-1002 (USD) — balance 12000.00, available balance not computable.availableBalance is nullable upstream: it is currentBalance minus thestructuredContent, envelope {"message": "Success.", "data": [...], "pagination": {...}}.nextToken andpreviousToken are always null and hasMore is always false; the upstream| Field | Type | Description |
|---|---|---|
corporateReference | string | The corporate that owns this account |
corporateCurrencyAccountReference | string | The account reference used by other tools |
currencyCode | string | ISO 4217 |
balance | decimal | Current balance |
availableBalance | decimal, or null | Balance minus outstanding instruction reservations; null when not computable |
get_account| Parameter | Type | Required | Description |
|---|---|---|---|
account_reference | string | yes | The corporate currency account reference |
content, a sentence per funding route:Account CCA-1001 (GBP): balance 48200.00, of which 47150.00 available. Local funding: active — ready to receive payments — pay into Northwind Trading Ltd, account 12345678, SortCode 04-00-75, at Modulr FS Limited (London branch), GB. International funding: not yet set up.structuredContent, envelope {"message": "Success.", "data": {...}}:| Field | Type | Description |
|---|---|---|
externalReference | string | The account reference |
currency | string | ISO 4217 |
currentBalance | decimal | Current balance |
availableBalance | decimal, or null | A computed value: currentBalance minus outstanding instruction reservations, null when not computable. The value it is computed from, outstandingInstructionBalance, is never itself serialized on the wire |
localAccountStatus | string | Locality status, see the enums appendix |
internationalAccountStatus | string | Locality status, see the enums appendix |
localPaymentDetails | object, or null | Pay-in details for the local route |
internationalPaymentDetails | array | Pay-in details for international routes; defaults to an empty list, may hold several options |
accountName, accountNumber,accountType, iban, routingCodeType, routingCode, bicCode, email,corporateReference, bankName, bankCountry, bankAddress,bankBranch.version only, an optimistic-concurrency counteraccount_reference must not be blank. This isget_account_ledgerlist_accounts).| Parameter | Type | Required | Description |
|---|---|---|---|
account_reference | string | yes | The corporate currency account reference |
from_date | string | no | Filter from this date (YYYY-MM-DD) |
to_date | string | no | Filter to this date (YYYY-MM-DD) |
limit | integer | no | Items per page (default 25, max 100) |
next_token | string | no | Cursor from the previous page's pagination.nextToken |
content, a markdown table, amounts signed for readability:| Date | Type | Amount | Counterparty | Description |
|---|---|---|---|---|
| 2026-08-14 | deposit | +5000.00 | Contoso Ltd | Invoice 4471 |
| 2026-08-15 | withdrawal | -1250.00 | Fabrikam GmbH | Supplier payment |
| 2026-08-16 | FX Trade | -800.00 | GBP to EUR conversion |
sourceReference for credit rows andbeneficiary for debit rows; the two are mutually exclusive upstream,structuredContent, envelope {"message": "Success.", "data": [...], "pagination": {...}},| Field | Type | Description |
|---|---|---|
amount | decimal | Unsigned amount. The sign shown in content is derived from direction |
direction | string | credit or debit in practice (see the enums appendix); typed as a plain string upstream, not an enforced enum |
type | string | Entry type. Only FX and forward legs get a friendly label; every other kind appears under its own raw snake_case value. See the enums appendix for the full set |
effectiveDate | date | Date the entry takes effect |
sourceReference | string, or null | Counterparty on credit entries |
beneficiary | string, or null | Counterparty on debit entries |
description | string, or null | Free text |
transactionReference | string, or null | Logical reference tying together every leg of one transaction |
transactionReference is kept in the compact response deliberately despiteaccount_reference must not be blank.list_instructions| Parameter | Type | Required | Description |
|---|---|---|---|
corporate_reference | string | one of | The corporate to list for |
cca_reference | string | one of | The account to list for |
completed | boolean | no | true for completed, false for in progress |
limit | integer | no | Max instructions (default 20, max 100) |
next_token | string | no | Cursor from the previous page |
corporate_reference or cca_reference is required.content, a markdown table:| Reference | Account | Type | Amount | Currency | Status | Created |
|---|---|---|---|---|---|---|
| INS-5512 | CCA-1001 | Payment | 1250.00 | GBP | Processed | 2026-08-15T09:12:44Z |
| INS-5513 | CCA-1002 | FXPayment | 800.00 | USD | Pending Settlement | 2026-08-16T11:03:02Z |
structuredContent, envelope {"message": "Success.", "data": [...], "pagination": {...}},| Field | Type | Description |
|---|---|---|
instructionReference | string | The instruction's external reference |
corporateReference | string | Owning corporate |
ccaReference | string | Source account. A corporate can hold several accounts in one currency, so currency alone does not identify it |
instructionType | string | See the enums appendix |
amount | decimal | Instruction amount |
currency | string | ISO 4217 |
displayStatus | string | User-facing status, see the enums appendix |
createdAt | datetime | Creation timestamp |
updatedAt | datetime | Last update timestamp |
get_instruction's full detailstatus, the internal instruction status. ThedisplayStatus alreadyError: You must provide either corporate_reference or cca_reference.Error: Provide only one of corporate_reference or cca_reference, not both.get_instruction| Parameter | Type | Required | Description |
|---|---|---|---|
instruction_reference | string | yes | The instruction reference to look up |
content, a sentence, with optional clauses appearing only when theInstruction INS-5513 (FXPayment) for corporate CORP-8842, account CCA-1002: 800.00 USD -> 742.10 EUR, status Pending Settlement. Beneficiary: BEN-3301. Sender reference (shown to the recipient): INV-4471. Purpose: SUPPLIER_PAYMENT. Fees: 4.20 FX, 1.50 transaction, 0.00 reseller. Created 2026-08-16T11:03:02Z, updated 2026-08-16T11:04:18Z.structuredContent, envelope {"message": "Success.", "data": {...}}.version, status, quoteId), leaving 22, plus 2 more synthesized onesrejectedBy, rejectionComment) present only when a| Field | Type | Description |
|---|---|---|
externalReference | string | The instruction reference |
createdAt | datetime | Creation timestamp |
updatedAt | datetime | Last update timestamp |
failureReason | string, or null | Why it failed, when it did |
displayStatus | string | User-facing status, see the enums appendix. Can be the synthesized value Rejected, described below -- not a raw backend value |
rejectedBy | string, or absent | Present only when displayStatus is the synthesized Rejected AND the approval lookup succeeded. The rejecting user's raw id (IdP scheme, tenant and principal id, e.g. waad|Sokin-Platform-UAT|7ca5ded2-...); this server has no name-resolution for it, which is also why it's left out of this tool's content text and only present here |
rejectionComment | string, or absent | Present under the same condition as rejectedBy. The rejecting approver's own comment (mandatory on their end) |
instructionType | string | See the enums appendix |
corporateReference | string | Owning corporate |
corporateCurrencyAccount | string | Source account |
amount | decimal | Source amount |
destinationAmount | decimal, or null | Destination amount; set only when a conversion happens |
sourceCurrency | string | ISO 4217 |
destinationCurrency | string | ISO 4217 |
beneficiaryReference | string, or null | The payee |
senderReference | string, or null | Text shown to the recipient |
paymentPurpose | string, or null | Purpose code |
paymentReference | string, or null | Payment reference |
sokinFxFee | decimal | FX fee |
sokinTransactionFee | decimal | Transaction fee |
resellerTransactionFee | decimal | Reseller fee |
destinationCorporateReference | string, or null | Destination corporate, when distinct from the source |
destinationCorporateCurrencyAccount | string, or null | Destination account, when applicable |
instructionRequestReference | string, or null | The request this instruction came from. Pass it to get_instruction_request |
version (an optimistic-concurrency counter),status (the internal status; the API's own field description calls itdisplayStatus), and quoteId (an internaldisplayStatus: "Rejected"rejectedBy/rejectionComment are this server's own inference, neverstructuredContent shows the raw displayStatus: "Failed" with neithercontent (built from the compact view regardlessdisplayStatus: "Failed". This server detects a rejection from an exact"Approval rejected: " prefix on failureReason and relabelsdisplayStatus to Rejected before returning it -- a value the backendfailureReason forGET /approval-evaluations/by-instruction/
{reference}) to find the rejecting decision and addsrejectedBy/rejectionComment. That second call can fail on its owndisplayStatus isRejected but the two extra fields are simply absent. ThisRejected is unrelated to get_instruction_request's own Rejected (seeget_instruction_request| Parameter | Type | Required | Description |
|---|---|---|---|
instruction_request_reference | string | yes | The instruction request reference exactly as the API returned it (currently IR--prefixed, e.g. IR-Payment-..., though treat the prefix as convention, not contract). Not an instruction reference; those belong to get_instruction. If you only have an instruction, its instructionRequestReference field is this value. |
status carefully: the names run counter to their meaning.Created is the initial, still-processing state, not a success. AcceptedRejected is terminal failure. These are the onlycontent, one sentence, branching on status:Instruction request IR-Payment-1 is still being processed -- no outcome yet. Check again shortly.Instruction request IR-Payment-1 was accepted: instruction INS-42 was created. Use get_instruction for its full detail.Instruction request IR-Payment-1 was rejected: Insufficient balance (category: amount). No instruction was created.createdInstructionReference is nullable on the contract even when theAccepted; in that case the sentence ends "accepted; the createdNone. A status outside the three above is echoed verbatim (...
has status <value>.) rather than reinterpreted.structuredContent, envelope {"message": "Success.", "data": {...}},| Field | Type | Description |
|---|---|---|
externalReference | string | The instruction request reference |
status | string | Created, Accepted, or Rejected; see the enums appendix |
createdInstructionReference | string, or null | The instruction produced on acceptance. Feed it to get_instruction. Nullable even when Accepted |
failureReason | string, or null | Why the request was rejected |
errorCategory | string, or null | The field or category that caused the rejection. See the enums appendix |
version, an optimistic-concurrency counter.externalReference or statusvalidate_payment_instructionerrorCategory:
"beneficiary_currency" and a reason naming the currency, instead of a falselist_beneficiaries before assumingcreate_fx_payment_instructionPOST upstream, and that is not a| Parameter | Type | Required | Description |
|---|---|---|---|
corporate_currency_account | string | yes | Source account reference from list_accounts. Currency and corporate are derived from it |
beneficiary_reference | string | yes | The payee, from list_beneficiaries. Must already exist |
amount | string | yes | Plain decimal, max 2 places, greater than zero (e.g. "125.50") |
reseller_fee | string | no | Same format as amount, zero or greater |
sender_reference | string | no | Shown to the recipient, max 18 characters |
payment_purpose | enum | no | One of 22 declared purpose codes (e.g. ACCOUNTS_PAYABLE); see the enums appendix. An unrecognized value is rejected by schema validation, not sent |
amount_is_fees_inclusive | boolean | no | true takes fees out of amount; default false adds them on top |
content, a verdict sentence:Validation passed: this payment would clear the creation checks. Total debit 1256.70 (5.20 Sokin fee, 1.50 reseller fee); beneficiary would receive 1250.00. Nothing was created and no money moved.Validation failed: insufficient available balance on the source account (category: amount). The engine still computed amounts: total debit 1256.70 (5.20 Sokin fee, 1.50 reseller fee). Nothing was created and no money moved.structuredContent, envelope {"data": {...}}:| Field | Type | Description |
|---|---|---|
success | boolean | Whether the dry run passed every check |
failureReason | string, or null | Why it failed |
errorCategory | string, or null | Machine-readable failure category. Not a closed set on the wire; see the enums appendix |
feeBreakdown | object, or null | Amounts, when the engine got far enough to compute them |
feeBreakdown carries amount (total debit including fees, required),destinationAmount (what the beneficiary receives, nullable), sokinFeeresellerFee (required), and feeRateCardReference (nullable).message field, the literal string"Success." on every call regardless of the verdict. It describes the HTTPError: corporate_currency_account must not be empty.Error: beneficiary_reference must not be empty.Error: amount must be a plain decimal number with at most 2 decimal places (e.g. '125.50'), got '1e3'.Error: amount must be greater than zero.Error: reseller_fee must be ... (same format rule, but zero is allowed)Error: sender_reference must be at most 18 characters, got 24."1e3" as 1000 and accept signed forms; for anything200 whose body lackssuccess verdict is rejected as a synthesized 502 with the messagecreate_payment_instructionerrorCategory:
"beneficiary_currency" as validate_payment_instruction above -- thisui://sokin-mcp/payment-review), asubmit_payment_instruction from inside the widget.validate_payment_instruction first. This tool's own descriptioncreate_payment_instruction and submit_payment_instruction per Vuk'sserver.py's own comment onUI_ONLY_TOOL_NAMES). The real human control for a payment is thecreate -> confirm
in chat -> submit flow.submit_payment_instruction.idempotencyKey in plain words -- structuredContent is not reliablysubmit_payment_instruction itself with that key and byte-identicalstructuredContent does not change between the two branches: idempotencyKeycontent changes.structuredContent, which was by itself enough for a caller tosubmit_payment_instruction's destructive annotation. Treat that prompt assubmit_payment_instruction's own entry for the fullvalidate_payment_instruction| Parameter | Type | Required | Description |
|---|---|---|---|
corporate_currency_account | string | yes | Source account reference from list_accounts. Currency and corporate are derived from it |
beneficiary_reference | string | yes | The payee, from list_beneficiaries. Must already exist — if it doesn't, create it first with create_beneficiary (needs a one-time emailed code) |
amount | string | yes | Plain decimal, max 2 places, greater than zero (e.g. "125.50") |
reseller_fee | string | no | Same format as amount, zero or greater |
sender_reference | string | no | Shown to the recipient, max 18 characters |
payment_purpose | enum | no | One of 22 declared purpose codes (e.g. ACCOUNTS_PAYABLE); see the enums appendix. An unrecognized value is rejected by schema validation, not sent |
amount_is_fees_inclusive | boolean | no | true takes fees out of amount; default false adds them on top |
status_check_key | string | no | Leave unset. Only the review widget passes it, with the idempotency key it was given, to ask whether that draft was already submitted. With it the tool prepares nothing and makes one read-only call, for the account; see "Status check" below |
content, when MCP UI is declared -- a not-submitted notice worded so aPayment prepared and validated -- awaiting the user's confirmation in the review widget. Nothing has been submitted; do not report this payment as made, and do not call submit_payment_instruction yourself.content, when MCP UI is not declared (or unknown) -- the idempotencyPayment prepared and validated -- nothing has been submitted yet. idempotency key: 3fae1c9e-... . Read the draft and fees back to the user and get their explicit approval; only after they approve, call submit_payment_instruction yourself with this idempotency_key and the exact same arguments. Never submit without that approval, and never tell the user the payment was made until submit_payment_instruction succeeds -- it will still need approval in the Sokin portal afterward.The payment could not be validated: insufficient available balance on the source account. Fix the inputs (validate_payment_instruction is the pre-flight) and prepare again. Nothing has been submitted.structuredContent: this tool builds its own payload, so there is no{"data": ...} envelope:| Field | Type | Description |
|---|---|---|
draft | object | The normalized payment exactly as it would be submitted |
currency | string, or null | The source account's currency, for display. Null if the lookup failed (display sugar that never blocks a valid draft). Absent entirely on the validation-failure branch |
accountIdentifier | string, or null | The account's visible identifier ("IBAN ..." or "account ..."), the same one shown in the Sokin portal. Null if the lookup failed |
beneficiaryDisplayName | string, or null | The payee's human-readable name, for display instead of the raw reference. Null if the lookup failed |
beneficiaryCountryCode | string, or null | The payee's country, alongside the display name. Null if the lookup failed |
validation | object | The dry-run verdict, same shape as validate_payment_instruction's data, including feeBreakdown |
idempotencyKey | string, or null | Server-minted, registered to this exact draft. Null when validation failed, because no approvable draft exists |
duplicateWarning | object | Present only when an identical draft was already submitted: instructionRequestReference and submittedAt of that earlier payment |
currency, accountIdentifier,beneficiaryDisplayName, beneficiaryCountryCode) come from twonull independently; a lookup failure neverdraft uses the tool's own snake_case parameter names, not the API'scorporate_currency_account, beneficiary_reference, amount,reseller_fee, sender_reference, payment_purpose,amount_is_fees_inclusive. It sits next to camelCase siblings likeidempotencyKey in the same object because it is the tool's argument set,payment_purpose; a whitespace-only optional becomes null.duplicateWarning is not an error. It means this exact payment hasinstructionRequestReference (waiting forsubmit_payment_instructionsender_reference, which makes it a different draft. There is novalidate_payment_instruction lists,status_check_key). Hosts replay this tool's storedstatus_check_key. With it, the tool answers from the server's sessioncorporate_currency_account with the caller's own token, and answers onlyexpired with no call at all, since no prepared draft can have one. The session store is shared by every user, so without| Field | Type | Description |
|---|---|---|
idempotencyKey | string | The key that was asked about, echoed back |
submissionState | string | submitted, in_flight (an identical submission is running now, or its outcome is unknown after a 5xx or timeout, so the payment may already exist; nothing is approvable), pending (not submitted, still approvable under this key) or expired (the pending draft expired, or the key was not registered for these arguments, and no submission is on record; the text says so rather than claiming nothing went out, since a lost record would look the same) |
duplicateWarning | object | Present only when submitted: instructionRequestReference, submittedAt (null if the server did not record the time) and submittedUnderThisKey (true when this key's own draft was submitted, false when an identical draft was submitted under another key). Submitted means Sokin accepted the request, not that the payment completed; the card follows the rest from instructionRequestReference |
content reads:Status check only -- nothing was prepared and nothing was submitted by this call. This payment draft has not been submitted; it is still awaiting the user's decision in the review widget. status_check_key is for the review widget only; omit it to prepare a payment.submissionState did not come from a status check: asubmitted answer that carries noinstructionRequestReference, showing "couldn't check" instead. If notools/call), the card shows "couldn't check" too; a reply that arrivessubmit_payment_instructioncreate_payment_instruction'screate_payment_instruction minted, and onlyreadOnlyHint: false, destructiveHint: true and idempotentHint: true,| Parameter | Type | Required | Description |
|---|---|---|---|
idempotency_key | string | yes | The key create_payment_instruction minted for this exact draft, from its idempotencyKey field |
corporate_currency_account | string | yes | Must match the reviewed draft |
beneficiary_reference | string | yes | Must match the reviewed draft |
amount | string | yes | Must match the reviewed draft |
reseller_fee | string | no | Must match the reviewed draft |
sender_reference | string | no | Must match the reviewed draft |
payment_purpose | enum | no | Must match the reviewed draft |
amount_is_fees_inclusive | boolean | no | Must match the reviewed draft |
content, a 202 receipt. Acceptance is not completion:Payment submitted: instruction request IR-Payment-77 for 125.50 from CCA-1 to BEN-1. This is an instruction-request reference, not a completed payment -- it now needs human approval in the Sokin portal before it is processed. Check the outcome via get_instruction_request.structuredContent, envelope {"data": {...}} with a single field on a| Field | Type | Description |
|---|---|---|
externalReference | string | The instruction request's reference. Pass it to get_instruction_request for the outcome |
202 whose body lacksexternalReference is rejected as a synthesized 502 reading "APImessage field on a freshmessage is already passed through in compact mode.| Situation | Result |
|---|---|
| Key unknown, fabricated, or expired | Error: unknown idempotency key -- payments are prepared via create_payment_instruction, which mints the key and renders the review widget. If a prepared draft was lost, prepare it again. |
| Arguments differ from the reviewed draft | Error: these arguments do not match the draft the user reviewed under this idempotency key. Only the exact reviewed draft can be submitted |
| Key already used, with different values | Error: this idempotency key was already used to submit a different draft (IR-...). Nothing was submitted for these arguments. Answering with the original's state would read as "your new values were handled" |
| Identical draft already submitted, under any key | Error: an identical payment was already submitted as IR-... at ... -- this submission was refused, no new payment was created. Permanent, not windowed |
| Identical draft being submitted right now | Error: an identical payment is being submitted right now ... Wait a moment, then check the payment's state with get_instruction_request. Concurrent approvals are resolved atomically, so exactly one can win |
This draft was already submitted -- no new payment was created. Instruction request IR-Payment-77 was accepted: instruction INS-9 was created. Use get_instruction for its full detail.structuredContent is get_instruction_request's shapeexternalReference, status, createdInstructionReference,failureReason, errorCategory), not the one-field submission shape. CodeidempotencyKey necessarily reaches the model increate_payment_instruction's structuredContent regardless of whether acreate_payment_instruction's own text asks for, plus that(idempotencyKey, type,
account), so duplicate money on one key is impossible even if theget_instruction_request and then get_instruction, rendering theget_instructionApproval Initiated, and it is not final: portal approval isui/update-model-context (MCP SEP-1865): once whenget_instruction_request directly.get_fx_quoteinitialize time. Thisui://sokin-mcp/fx-quote), ancreate_fx_quote). Minting one before a human ever sees a reviewcorporate_currency_account is passed itcreate_fx_quote; its "Settle" buttonsettle_fx_quote. Do not tell the user a rate exists from thiscreate_fx_quote orsettle_fx_quote; see settle_fx_quote's hard refusal below. (This is| Parameter | Type | Required | Description |
|---|---|---|---|
buy_currency | string | yes | ISO 4217 code being bought (e.g. "USD") |
sell_currency | string | yes | ISO 4217 code being sold (e.g. "GBP") |
amount | string | yes | The amount of whichever side fixed_side names |
fixed_side | "BUY" or "SELL" | no | Which side amount refers to. Default "SELL" |
corporate_reference | string | yes | The corporate to quote for. Always pass it: the upstream API only fills it in when the token itself names one corporate, and then it ignores what you pass, so passing it is never wrong. Call list_corporates first if the user hasn't named one |
corporate_currency_account | string | no | The account to eventually settle from. Used by the card's later Settle step; when passed, the card-rendering branch also reads this account once to label it (the raw reference is never sent anywhere else by this call) |
card_id | string | no | Leave unset. Only the FX quote card passes it, with the cardId from this tool's own earlier result, to ask what already happened on that card. With it the call is a status check, not a card; see "Card status check" below |
content is a one-sentence acknowledgement, not a quote:Ready to quote GBP -> USD, 1000.00 on the sell side. No quote has been created yet -- the card's Get Quote button mints one.structuredContent is a flat echo of the request, not upstream data:| Field | Type | Description |
|---|---|---|
buyCurrency | string | Echoed request |
sellCurrency | string | Echoed request |
amount | string | Echoed request |
fixedSide | string | Echoed request |
corporateReference | string, or null | Echoed request |
corporateCurrencyAccount | string, or null | Echoed request |
cardId | string | A fresh id for this card (a uuid4). Minting it writes nothing. It is the card's only handle for asking, after a reopen, what already happened on it |
corporateCurrencyAccountDisplay | string | The resolved label, e.g. GBP account 12345678 or GBP IBAN GB33..., from a read of the settlement account. When that lookup fails or yields no identifier it is <sellCurrency> account (number unavailable) and the card data is still returned, never an error. Present only when corporate_currency_account was passed (non-blank); omitted otherwise, and then no upstream call is made at all. The card's requested state shows this label, never the internal reference |
card_id). Hosts replay this tool's stored resultcreate_fx_quote and settle_fx_quote, and those replies are not storedsettle_fx_quote.)card_id. create_fx_quote(card_id=)settle_fx_quote records itscard_id, the tool answers from the server's session store only. Itcard_id the servercreate_fx_quote), and neither unquoted nor that quote's own state isget_fx_quote.quoted state the reply carries every argument settle_fx_quotesettle_fx_quote for thatsettle_fx_quote also still refuses a session| Field | Type | Description |
|---|---|---|
cardId | string | The card that was asked about, echoed back |
cardState | string | unquoted (no quote from this card is on record: never quoted, its record expired, or not this user's card), quoted (the latest quote has no settlement on record and is inside its expiresAt; the card shows it live with Settle), expired (the latest quote has no settlement on record and is past its expiresAt; the card offers Get Quote again), settling (a settlement of it is running, or was attempted and its outcome never became known, so the conversion may exist; nothing is offered) or settled (a settlement from this card was submitted to Sokin as an instruction request; that is where the conversion starts, not proof it completed: the card resumes tracking it, through any wait for approval in the Sokin dashboard, until it is processed or fails) |
quote | object | Present for every state but unquoted: the quote as create_fx_quote returned it. It carries idempotencyKey only when quoted, the one state that offers Settle, and it is null when settled was recorded without the card's quote |
instructionRequestReference | string | Present only when settled: the IR- reference to follow with get_instruction_request |
create_fx_quotecard_id below). A press anywherecontent reads:Status check only -- nothing was quoted or settled by this call. A conversion from this card was already settled as instruction request IR-FX-6b2f90. Do not quote or settle it again; check get_instruction_request for its outcome. card_id is for the FX quote card only; omit it to show a card.settling for good. A settlement thatsettle_fx_quote keeps its claimquoted with a live Settle, andexpired with Get Quote, a new key that thesettle_fx_quote records the attempt onsettling however long ago it was, even after thesettled. To startget_fx_quote for a new card.quoted and expired mean "no settlement on record", not "nothing wasexpired never says nothing was settled: a store that lost acardState did not come from a status check: a serverunquoted. A storecardId and rendercontent tells the modelThis client cannot show the FX quote card, so no quote was created and no rate is shown. Open https://portal.uat.sokin.com/v2/transfer/currency-exchange to see the live rate and execute GBP -> USD for 1000.00 (SELL side) for corporate CORP-8842. Do not try to quote or settle this through the assistant.Open <url>.structuredContent, flat with no data envelope, unlike every other tool| Field | Type | Description |
|---|---|---|
buyCurrency | string | Echoed request, uppercased |
sellCurrency | string | Echoed request, uppercased |
amount | string | Echoed request |
fixedSide | string | Echoed request |
corporateReference | string | Echoed request |
indicative | boolean | Always true |
quoteCreated | boolean | Always false |
dashboardUrl | string, or null | The configured portal link, null when not configured |
quoteId, rate, fee or expiry appears anywhere in this response, andisError result. create_fx_quote and create_fx_payment_instruction| Failure | Message |
|---|---|
buy_currency / sell_currency is not 3-4 uppercase letters (checked first, buy then sell) | Error: buy_currency must be a currency code such as 'USD', got 'usd1'. |
buy_currency / sell_currency is well-formed but not a supported currency | Error: buy_currency 'ZZZ' is not a currency Sokin supports (e.g. 'USD', 'EUR', 'GBP'). |
| Same currency on both sides | Error: buy_currency and sell_currency must differ (both are 'USD'). |
amount is not a plain decimal (negative, 1e12, three decimals, non-ASCII digits) | Error: amount must be a plain decimal number with at most 2 decimal places (e.g. '125.50'), got '-5'. |
amount is zero | Error: amount must be greater than zero. |
amount is above the quote limit | Error: amount must not exceed 9999999999 (the FX quote limit), got '10000000000'. |
USDT and USDC), not all of ISO 4217. It is a copyCurrencyCode enum, so a currency upstream adds is refusedcreate_fx_quoteget_fx_quoteget_fx_quoteget_fx_quote's display step, this is a genuine write: every callMIN_2, the longest option the API's ownFXValidityPeriod enum offers) is only a fallback that applies when avalid_until overrides it in either direction. In practice the two liveMIN_2 window, the other returns its own valid_untilexpiresAt is the only figure to trust, never thefxFees) is already inside sellAmount: the provider isfxFees is denominated in the sell currency, whichever side is fixed.fxRate is the all-in customer rate, buyAmount / sellAmount byfxRate approximates buyAmount with thebuyAmount from the rate.buyAmount is the quote's locked buy leg and is the amount that settles.FX <fee> <sell currency>, included in the <sell amount> <sell currency> you sell,fxFees is present and positive. The other shapes fxFeesNo FX fee applies; <buy amount> <buy currency> is the amount that settles.;fxFees): the card shows "none" and the textreadOnlyHint: false, destructiveHint: false, idempotentHint:
false: a genuine third state, distinct from a typical write. Additive (itError: this session has not declared MCP UI support, so creating a real FX quote is refused here -- there is no card to display it in. Direct the user to the Sokin dashboard to get a live rate and execute a trade instead.| Parameter | Type | Required | Description |
|---|---|---|---|
buy_currency | string | yes | ISO 4217 code being bought |
sell_currency | string | yes | ISO 4217 code being sold |
amount | string | yes | The amount of whichever side fixed_side names |
fixed_side | "BUY" or "SELL" | no | Default "SELL" |
corporate_reference | string | yes | Same resolution rule as get_fx_quote, mandatory for the same reason |
corporate_currency_account | string | no | The account settlement would debit. Never sent to the quote-creation endpoint itself, only carried into the reserved draft for the later Settle call. When omitted, the corporate's own account in the sell currency is resolved automatically if there is exactly one match; with zero or several candidates, no account is reserved and the card renders with no Settle option |
reseller_fee | string | no | Carried into the reserved draft only, never sent to the quote-creation endpoint |
beneficiary_external_reference | string | no | Only needed if quote creation itself asks for one; some rate providers require it even though this quote never pays a beneficiary |
card_id | string | no | Leave unset. Only the FX quote card passes it, so that a reopened card can still show this quote and whether it was settled (see get_fx_quote's card status check). With it, the card is looked up first, from the store alone, and only a card with no quote on record or whose last quote expired with no settlement on record gets a new one. A card holding a live quote, already settled, possibly settling, or whose record was made for a different request gets no new quote and nothing is sent upstream (the error reads "no new quote was created from this card"), and a failure of that lookup is an error with nothing minted. The card's quote is then recorded with the settlement draft, and a failure there is reported like a failed draft registration ("could not be tracked"): a key that can be settled must be findable from its card. A card_id that is not a uuid is ignored, and the quote is simply not remembered |
content, a sentence naming the quote and its real expiry, followed (when the quote carries an FX fee) by a sentence saying the fee is inside the sell amount and which amount settles:Quote QT-88213 created: 1000.00 GBP -> 1187.40 USD at rate 1.18740. Expires at 2026-08-27T14:34:00+00:00 -- settle it before then, or ask for a fresh quote once it expires. The 2.50 GBP FX fee is included in the 1000.00 GBP sold; 1187.40 USD is the amount that settles.structuredContent, flat, no data envelope:| Field | Type | Description |
|---|---|---|
quoteId | string | Quote identifier, required by settle_fx_quote |
idempotencyKey | string | Minted here, not an upstream field — the exact key settle_fx_quote requires |
currencyPair | string | Sell/buy with a separator, e.g. GBP/USD — the V2 quote domain's format |
fxRate | string | The quoted rate |
fixedSide | string | Which side the amount was fixed on |
buyAmount | string | Amount bought |
buyCurrency | string | ISO 4217 |
sellAmount | string | Amount sold |
sellCurrency | string | ISO 4217 |
fxFees | string | FX fee |
createdAt | string | Normalized to UTC ISO-8601 with an explicit offset |
expiresAt | string | The real expiry, normalized the same way. The only expiry to trust; provider-set, so it may be far longer or shorter than the requested window (see above) |
corporateCurrencyAccount | string, or null | The resolved (or supplied) settlement account, or null if none could be resolved |
corporateCurrencyAccountDisplay | string, or null | Currency + account number or IBAN the portal shows (e.g. USD account 12345678, GBP IBAN GB33...); falls back to <currency> account (number unavailable) when the account lookup fails or yields no identifier. Null when no settlement account resolved. This is what the card's "Settle from" row renders; the raw reference never appears there |
resellerFee | string, or null | Echoed from the request, or null |
messageidempotencyKey,corporateCurrencyAccount, corporateCurrencyAccountDisplay and resellerFee are synthesized by this tool,Error: quote QT-88213 was created but could not be tracked -- it cannot be settled through this assistant. Do not report a usable quote to the user; ask for a fresh one via get_fx_quote.card_id, a card that already holds a live quote, is settled orError: no new quote was created from this card -- it already holds a live quote, a conversion from it is already settled, a settlement of it may still be in progress, or it was made for a different request. Do not quote from this card again; the card shows its current state. For another conversion, call get_fx_quote to show a new card.corporate_reference check and the capability refusal above.settle_fx_quoteinitialize time alsotools/list at all, but that omission is hygieneError: this session has not declared MCP UI support, so real-money FX settlement is refused here. Direct the user to the Sokin dashboard to execute this trade instead.create_payment_instruction/submit_payment_instructioncreate_fx_payment_instruction/submit_fx_payment_instruction), thiscreate_fx_quote still needs it so that no session mints real quotes withserver.py's UI_ONLY_TOOL_NAMES set names exactlycreate_fx_quote.| Parameter | Type | Required | Description |
|---|---|---|---|
idempotency_key | string | yes | The key minted by create_fx_quote for this exact quote, its idempotencyKey field |
corporate_currency_account | string | yes | Must match the quote the user reviewed |
quote_id | string | yes | Must match the quote the user reviewed |
reseller_fee | string | no | Must match the quote the user reviewed |
create_fx_quote reserved underidempotency_key. Any mismatch is refused, never silently corrected.create_fx_quote registers is held for 24create_fx_quote),Error:
this quote could not be settled: <reason>, the true reason, rather thanexpiresAt, andcreate_fx_quote keeps refusing a new quote on the card.settling instead of offering the conversion again (seeget_fx_quote's card status check). If the attempt cannot be recorded,Error: this quote
could not be settled: the attempt could not be recorded, so nothing was
sent. Try again in a moment. The record does not block a same-key retry:get_instruction_request over a repeatsettle_fx_quote call. It is a read, and it answers regardless of howidempotencyKey is also sent on the wire, and(idempotencyKey, instructionType,
corporateCurrencyAccount). A replayed key returns the first request andcreate_fx_quote's reply, if the model calls that tool itself (MCPget_fx_quote(card_id=)'s quoted reply. That callchannel == MCP andUNKNOWN, which is deliberately not gated, so ARC-651 mustcontent on success:FX conversion submitted: instruction request IR-FX-9911 for account CCA-1002. This is a submission receipt, not confirmation the conversion completed -- check get_instruction_request for its actual outcome.structuredContent, a data envelope, the same shape the upstream| Field | Type | Description |
|---|---|---|
data.externalReference | string | The instruction request reference. Pass to get_instruction_request for its actual outcome |
data.status | string | Initial status, e.g. "Created" |
message field back.| Condition | Message |
|---|---|
| Session not MCP-UI-capable | Error: this session has not declared MCP UI support, so real-money FX settlement is refused here. Direct the user to the Sokin dashboard to execute this trade instead. |
Empty idempotency_key | Error: idempotency_key must not be empty. |
| Unknown key (never registered, or expired) | Error: unknown idempotency key -- quotes are prepared via create_fx_quote, which mints the key and renders the review card. If a quote was lost, ask for a fresh one via get_fx_quote. |
| Arguments do not match the reserved draft | Error: these arguments do not match the quote the user reviewed under this idempotency key. Only the exact reviewed quote can be settled -- for a different conversion, get a fresh quote. |
| Another settlement for the same key is already in flight | Error: another settlement attempt for this exact quote is already being processed -- its outcome is not yet known. Do not retry immediately; check get_instruction_request shortly, or wait a moment before trying again. |
| Same key already settled | Not an error: returns the original get_instruction_request result again, prefixed "This quote was already submitted, no new conversion was created." |
| Quote fails a synchronous pre-check (e.g. expired) | Error: this quote could not be settled: <reason>. Adds Ask for a fresh quote via get_fx_quote. when the failure category is the quote itself |
| B2B API rejects with 4xx | Standard B2B API error shape. The reservation is released, so a corrected retry does not have to wait out its TTL |
| B2B API fails with 5xx, or times out, or the reply to the create call cannot be read | Error: the settlement submission's outcome is unknown (API error <status>: <message>, or "the response could not be read") -- it may or may not have been accepted. Do not report this conversion as settled or as failed, and do not retry immediately; check the account's recent instruction requests first. The reservation is deliberately kept, not released, so a blind retry cannot double-submit, and the card's attempt record (written before the request) stays, so the card keeps reading settling after the reservation lapses |
create_fx_payment_instructionPOST /instruction-requests/fx-payment on submit.create_payment_instruction, combining the account/beneficiary side of aquote_id. Earlier it required a livecreate_fx_quote call. Per Vuk'squote_id minted by an earliercreate_fx_quote call back into its own context (that depends onui/update-model-context reaching it, which is not guaranteed for aget_fx_quote.buy_currency,sell_currency, amount, fixed_side, corporate_reference) and mints acreate_fx_quote uses (POST /fx/quotes/, the V2 quote domain). The B2BbeneficiaryExternalReference, is equivalent to the old two-call flow andcreate_fx_quote:get_fx_quote step first, whenever theget_fx_quoteinstructionType: FXPayment theui://sokin-mcp/fx-payment-review), a review card with Approve andsubmit_fx_payment_instruction from inside the widget. This tool rendersvisualize tool.readOnlyHint: true, and its own source comment justifies that by sayingcreate_payment_instruction uses. That comment predates the quote-mintcreate_fx_quote itself isreadOnlyHint: false for exactly this kind of upstream write;create_fx_payment_instructioncreate_fx_quote already asks for.create_payment_instruction's.Rejected, with a failure reason), notcreate_payment_instructionstructuredContent carries idempotencyKey| Parameter | Type | Required | Description |
|---|---|---|---|
corporate_currency_account | string | yes | Source account reference from list_accounts |
beneficiary_reference | string | yes | The payee, from list_beneficiaries. Must already exist — if it doesn't, create it first with create_beneficiary (needs a one-time emailed code). Passed to the quote provider (as beneficiaryExternalReference) as well as the payment itself |
buy_currency | string | yes | ISO 4217 code being bought (e.g. "USD") |
sell_currency | string | yes | ISO 4217 code being sold (e.g. "GBP") |
amount | string | yes | The amount of whichever side fixed_side names |
fixed_side | "BUY" or "SELL" | no | Which side amount refers to. Default "SELL" |
corporate_reference | string | no | The corporate to quote for. Required in practice; call list_corporates first if the user hasn't named one. Omitting it does not auto-resolve for a typical caller -- the upstream API only fills it in when the token itself already identifies one specific corporate (a corporate's own self-service token, or a reseller token delegating as that one corporate), never for a plain reseller token. Used only for minting the quote, never sent to the payment itself |
reseller_fee | string | no | Plain decimal string, zero or greater |
sender_reference | string | no | Shown to the recipient, max 18 characters |
payment_purpose | enum | no | Same 22 declared values as create_payment_instruction; see the enums appendix. An unrecognized value is rejected by schema validation, not sent |
status_check_key | string | no | Leave unset. Only the review widget passes it, with the idempotency key it was given, to ask whether its draft was already submitted. With it the tool mints no quote and makes one read-only call, for the account; see "Status check" below |
content, when MCP UI is declared -- a not-submitted notice:FX payment prepared and validated -- awaiting the user's confirmation in the review widget. Nothing has been submitted; do not report this payment as made, and do not call submit_fx_payment_instruction yourself.content, when MCP UI is not declared (or unknown) -- unlikecreate_payment_instruction, submit's arguments here are NOT the same asquote_id, which is not one of create's ownquote_id, and the realstructuredContent, which a non-UI client is not guaranteed toFX payment prepared and validated -- nothing has been submitted yet. Total debit 100.00 (0.50 Sokin fee, 0.00 reseller fee); beneficiary would receive 125.00 (GBP debited, beneficiary receives USD). idempotency key: 9c21ab4f-... . quote_id: FXQ-1a2b3c... . Read these figures back to the user and get their explicit approval. Only after they approve, call submit_fx_payment_instruction with this idempotency_key, this quote_id, and the same corporate_currency_account, beneficiary_reference, reseller_fee, sender_reference, and payment_purpose you used here -- it does not take buy_currency, sell_currency, amount, fixed_side, or corporate_reference; those only mint the quote. Never submit without that approval, and never tell the user the payment was made until submit_fx_payment_instruction succeeds -- it will still need approval in the Sokin portal afterward.The FX payment could not be validated: <reason>. Nothing has been submitted.errorCategory is "quote" -- a defensiveError: the quote minted for this request is no longer valid (<reason>). Call create_fx_payment_instruction again to mint a fresh one. Nothing has been submitted.Error:, none of the three texts above setisError: true -- they are reports from the validation dry run, deliveredcreate_payment_instruction's own validation-failure text. See the top-levelstructuredContent: this tool builds its own payload, no{"data": ...} envelope:| Field | Type | Description |
|---|---|---|
draft | object | The normalized FX payment exactly as it would be submitted: corporate_currency_account, beneficiary_reference, quote_id, reseller_fee, sender_reference, payment_purpose. No amount, buy_currency, sell_currency, or fixed_side field -- the ledger legs derive entirely from the quote, and the pricing inputs exist only to mint that quote, never to submit the payment. quote_id here is the id of the quote THIS call minted for you, not one you supplied -- pass it through unchanged to submit_fx_payment_instruction |
currency | string, or null | The source account's currency, for the card's debit and fee rows. Null when the account lookup failed; display only, it never blocks a valid draft |
buyCurrency | string | The beneficiary's currency (this call's own buy_currency parameter, normalized), labelling what the beneficiary actually receives. Always present, unlike currency -- there is no lookup that can fail |
quoteExpiresAt | string, or null | The minted quote's normalized expiry timestamp, straight from the adapter -- drives the card's countdown and disables Approve once it passes. Null only if the upstream response omitted it |
quoteCreatedAt | string, or null | The minted quote's normalized creation timestamp, paired with quoteExpiresAt for the countdown's total span |
validation | object | The FX-payment validate dry-run verdict (instructionType: FXPayment), same envelope shape as validate_payment_instruction's, including a resolved feeBreakdown |
idempotencyKey | string, or null | Server-minted, registered to this exact draft. Null when validation failed |
duplicateWarning | object | Present only when an identical draft was already submitted: instructionRequestReference and submittedAt |
quoteRequest | object | The pricing arguments this draft was prepared from, under the tool's own parameter names: buy_currency, sell_currency, amount, fixed_side, corporate_reference. Absent on the validation-failure branch. The review card needs these to rebuild this tool's required arguments for its status check |
duplicateWarning is not an error, with the same semantics ascreate_payment_instruction's: when a widget is present it renders ansender_reference.corporate_currency_account orbeneficiary_reference, a malformed reseller_fee, an over-lengthsender_reference) is checked and rejected with isError: true beforecreate_fx_quote, a failure registering the draft after the quote hasstatus_check_key). The same mechanism ascreate_payment_instruction's, with the same response fields and states.draft (without quote_id) plus quoteRequest, and thequote_id, because every prepare mints a fresh one and twoquoteRequest existed cannot rebuild those arguments; itsubmit_fx_payment_instructioncreate_fx_payment_instruction's own text handscreate_fx_payment_instruction first -- it mints the quote and the keyPOST
/instruction-requests/fx-payment, and cannot be cancelled once submitted.idempotency_key minted by create_fx_payment_instruction,| Parameter | Type | Required | Description |
|---|---|---|---|
idempotency_key | string | yes | The key minted by create_fx_payment_instruction for this exact draft |
corporate_currency_account | string | yes | Must match the reviewed draft |
beneficiary_reference | string | yes | Must match the reviewed draft |
quote_id | string | yes | Must match the reviewed draft -- this is the id of the quote create_fx_payment_instruction minted for you, from its draft.quote_id field, not a value you choose |
reseller_fee | string | no | Must match the reviewed draft |
sender_reference | string | no | Must match the reviewed draft |
payment_purpose | enum | no | Must match the reviewed draft |
content on success:FX payment submitted: instruction request IR-FXPayment-77 from CCA-1 to BEN-1. This is an instruction-request reference, not a completed payment -- it now needs human approval in the Sokin portal before it is processed. Check the outcome via get_instruction_request.settle_fx_quote's "What the gate does not guarantee"get_instruction_request.structuredContent, {"data": {"externalReference": ...}} on a freshsubmit_payment_instructionexternalReference); on a replay of an already-submitted draft, the sameget_instruction_request shape (externalReference, status,createdInstructionReference, failureReason, errorCategory) instead ofmessage field on a freshmessage through in compact mode.submit_payment_instruction: a 202 whose body lacks externalReference502 reading "API returned an unexpectedsubmit_payment_instruction lists (unknown key, submitted-draft mismatch,list_beneficiariescreate_beneficiary — the APIdisplayName, entityupdate_beneficiary; there is no delete| Parameter | Type | Required | Description |
|---|---|---|---|
corporate_reference | string | no | The corporate to list for. Required in practice |
search | string | no | Text to match against the display name. Omit to list all |
search_mode | "contains" or "starts_with" | no | How search matches. Default "contains" |
limit | integer | no | Max per page |
next_token | string | no | Cursor from the previous page |
page | integer | no | Not supported; accepted only to reject it clearly |
offset | integer | no | Not supported; accepted only to reject it clearly |
content, a markdown table:| Reference | Name | Type | Country | Currencies (rails) | Status |
|---|---|---|---|---|---|
| BEN-3301 | Fabrikam GmbH | company | DE | EUR (sepa/swift) | active |
| BEN-3302 | Jane Okafor | individual | GB | GBP (faster_payments) | active |
structuredContent, envelope {"data": [...], "pagination": {...}},| Field | Type | Description |
|---|---|---|
externalReference | string | The beneficiary reference |
displayName | string | Display name |
entityType | string | individual or company |
countryCode | string | ISO country code |
destinations | array | Each entry {currencyCode, paymentRail} |
status | string | active or deleted |
(currency, rail) pair is kept rather than deduped to a barefaster_paymentsswift differs in settlement speed and cost, and collapsing that wouldisInternal, supportedRails, createdAt,version, the individual / company detail objects, address, and thevalidationStatus, autodepositEnrolled. It also addsmessage field (the literal "Success."). One field issecurityAnswer (the answer a recipientError: page/offset pagination is not supported here. Use limit and next_token instead.get_beneficiarylist_beneficiaries(currency, rail) pairs, this returns everyvalidationStatus.| Parameter | Type | Required | Description |
|---|---|---|---|
beneficiary_id | string | yes | The payee's externalReference from list_beneficiaries |
corporate_reference | string | no | The corporate this payee belongs to. Required in practice |
content — a header line, a destinations table, and the creation date:| Currency | Rail | Validation | Account |
|---|---|---|---|
| GBP | faster_payments | matched | 12345678 |
| CAD | e_transfer | not_validated | payee@example.com |
deleted gets an explicit trailing line: it cannotstructuredContent — envelope {"data": {...}}:| Field | Type | Description |
|---|---|---|
externalReference | string | The beneficiary reference |
displayName | string | Display name |
entityType | string | individual or company |
countryCode | string | ISO country code |
status | string | active or deleted |
isInternal | boolean | Whether this is a Sokin-to-Sokin payee |
destinations | array | Full per-method payloads: currencyCode, paymentRail, validationStatus, and the method's own account fields. Bank-account destinations carry their stored snake_case keys (e.g. account_number, institution); e-Transfer and crypto destinations use camelCase |
individual / company | object | Identity details — only the one matching entityType, and only when set |
address | object | Payee address, when set |
createdAt | string | Creation timestamp |
supportedRails,[] (rail availabilitydestinations[].paymentRail instead), and version, an internalmessage, supportedRails (alwaysversion, and the raw destination payloads — minus thesecurityAnswer, which is withheld even from the full responseError: beneficiary_id must not be empty.That reference is not available on this account. Check BOTH beneficiary_id (the Reference column from list_beneficiaries) and corporate_reference (the value list_corporates returned), and retry. If the reference is correct, this account may not have access to it. — one rendering for every reference failure: an unknown payee, an unknown corporate, a corporate belonging to someone else, or a permission denial. It names both candidate arguments and never which one upstream rejected. This tool used to tell an unknown corporate apart from an unknown payee by matching the phrase "corporate not found" in the upstream text; that was removed because upstream 404s a corporate reference that does not exist but 403s one that exists and is foreign, so the distinction answered "does that corporate exist".get_beneficiary_schemaaddress.city, bankAccount.accountNumber).list_beneficiaries destinations. Every rail belongs to exactly onee_transfer → e_transfer, internal → sokin_internal, pix →virtual_account, the on-chain networks (base, tron, ethereum,polygon, solana) → crypto, and every other rail (faster_payments,sepa, swift, ach, eft, …) → bank_account.| Parameter | Type | Required | Description |
|---|---|---|---|
payment_method | "bank_account" | "e_transfer" | "virtual_account" | "sokin_internal" | yes | See the rail-to-method mapping above. The taxonomy's fifth method, crypto, is deliberately not an accepted value — the upstream endpoint cannot describe crypto payees yet (its currency validation caps currencyCode at 3 characters; the only crypto currencies are the 4-character stablecoin codes USDC/USDT), so there is no schema to fetch. Crypto payees skip the schema lookup: currency_code USDC/USDT, payment_rail = the on-chain network, wallet address in the crypto object's walletAddress |
currency_code | string | unless sokin_internal | ISO 4217 currency the payee is paid in |
country_code | string | unless sokin_internal | ISO 3166-1 alpha-2 country of the payee's account |
entity_type | "individual" | "company" | unless sokin_internal | Payee entity type |
autodeposit_enrolled | "true" | "false" | "unknown" | no | e-Transfer only: whether the recipient email has Interac auto-deposit. Passing "false" or "unknown" makes the security question/answer fields required; omitting the parameter behaves like "true" (they stay optional), so pass "unknown" rather than omitting when enrolment hasn't been checked |
corporate_reference | string | no | The corporate the payee will be created under. Required in practice |
content — a markdown table:| Field | Type | Required | Notes |
|---|---|---|---|
| displayName | string | yes | length 1-100 |
| bankAccount.accountNumber | string | only for faster_payments | |
| entityType | enum | yes | one of: individual, company |
| eTransfer.securityQuestion | string | no | conditional on eTransfer.email; fixed value: Confirm the agreed passphrase |
requiredForPaymentRails or viaconditions, as with bankAccount.institution.branchCode onbankAccount.accountNumberType rendersone of: account_number, iban; account_number only with chaps,
faster_payments, swift; iban only with swift, so a faster_payments payeeaccount_number. Single-rail corridors carry no conditions andstructuredContent — envelope {"data": {"condition": {...}, "fields": [...]}}.condition echoes the resolved query context (currencyCode, entityType,countryCode, paymentMethod). Each field object always carries name,fieldType (string / integer / boolean / date / enum), andisRequired (a non-nullable boolean upstream, default true — never absent),min, max,allowedValues, requiredForPaymentRails, conditions, visibility,conditionalOn, revalidateOn, fixedValue.message, and regional — which theError: currency_code, country_code, entity_type required for payment_method 'bank_account' (only sokin_internal can omit them). — mirrors the API's own rule; for sokin_internal the API defaults the entity type to company.payment_method outside the four accepted values (including crypto — see the parameter table) fails MCP argument-schema validation before the tool body runs.That reference is not available on this account. Check corporate_reference (the value list_corporates returned), and retry. If the reference is correct, this account may not have access to it. — corporate resolution runs upstream before the schema lookup, and corporate_reference is this tool's only reference argument, so the neutral rendering can name it without disclosing which failure it was.API error (400): No payment rails for GBP+US and bank_account.validate_beneficiarycreate_beneficiary's docstring requires the applicablematched/close_match/check_not_possible (not_validated, no_match,invalid all block). Exactly one verification mode per call:| Mode | Fields | What runs |
|---|---|---|
| Existing payee | beneficiary_id | Re-validates every destination and persists each refreshed validationStatus — the reason this tool is not read-only. Convergent: re-running re-derives the same statuses. Corridor-gated per destination: e-Transfer gets a DDR enrolment probe, internal and non-EUR bank destinations come back not_validated ("not applicable"), GBP local rails get CoP where reseller-enabled, EUR gets VoP |
| e-Transfer probe | email | Interac auto-deposit (DDR) check. No persistence |
| IBAN name check | iban + bic + account_name | Verification of Payee — runs on any format-valid IBAN regardless of country; verdicts are matched / close_match / no_match / check_not_possible, never not_validated in this mode. A malformed IBAN or BIC comes back invalid |
| UK CoP | sort_code + account_number + account_name | Confirmation of Payee on GBP local rails — only where enabled for the reseller; otherwise not_validated |
| Parameter | Type | Required | Description |
|---|---|---|---|
beneficiary_id | string | mode 1 | The payee's externalReference from list_beneficiaries |
email | string | mode 2 | Recipient email for the auto-deposit check |
iban | string | mode 3 | IBAN (15–34 chars) |
bic | string | mode 3 | BIC/SWIFT, 8 or 11 chars (upstream uppercases) |
account_name | string | modes 3, 4 | The account holder name to verify |
sort_code | string | mode 4 | UK sort code, 6 digits — spaces/hyphens tolerated |
account_number | string | mode 4 | UK account number, 8 digits |
corporate_reference | string | no | The corporate to validate under. Required in practice |
content — a worst-status-first summary line and a results table:| Currency | Rail | Check | Status | Name on record | Details |
|---|---|---|---|---|---|
| COP | close_match | J Okafor Ltd | Name is a close match |
no_match/invalid render as NOTcheck_not_possible/not_validated render as NOT a pass (theWorst of N checks: so a mixed payee never reads as "nothing ran". A matchedAll checks passed. with a note that DDRautodeposit_enrolled value to pass toget_beneficiary_schema, and that enrolment state's e-Transfer field list.structuredContent — envelope {"data": {...}}:| Field | Type | Description |
|---|---|---|
isValid | boolean | False only when some result is no_match or invalid. True does not mean verified — check_not_possible and not_validated still yield true |
results | array | One entry per check: validationType (FORMAT/VOP/COP/DDR), status, and — when set — currencyCode, paymentRail, details, actualName (the name on record, on a CoP partial/close match) |
autodepositEnrolled | boolean | Email mode only; absent when the check was inconclusive |
registrationName / legalName | string | Email mode, only when enrolled |
securityQaRequired | boolean | Email mode: whether the payee needs a security question/answer |
fields | array | Email mode: the e-Transfer schema fields for that enrolment state (same shape as get_beneficiary_schema) |
message field only.Error: supply fields for exactly one verification mode: beneficiary_id (existing payee); email (e-Transfer auto-deposit); iban + bic + account_name (IBAN name check); or sort_code + account_number + account_name (UK Confirmation of Payee).Error: the IBAN name check needs all of iban, bic and account_name.Error: the UK Confirmation of Payee check needs all of sort_code, account_number and account_name.No payee with id 'ben_missing' exists under this corporate -- check BOTH beneficiary_id ... and corporate_reference ... — this tool keeps the corporate-vs-id 404 split that get_beneficiary dropped (see ARC-610 in the decision notes); its corporate branch now renders a fixed Corporate not found string rather than quoting the upstream message.results/isValid is reported asAPI error (502) — "the result is unknown; do not report the details ascreate_beneficiaryotp_code) dry-runsPOST /beneficiaries/batch-validation — an invalid draftotp_required. The second call repeats the same argumentsotp_code. For a corporate without the OTP requirement, the first callotp_code while any code isotp_resent instead:list_beneficiaries first; the toolvalidate_beneficiary name check beforeno_match/invalid/close_match until the user has seen both names. This is instruction to the calling model, notvalidate_beneficiary rather than being embedded in the create flow). Itbeneficiary_id mode after creatingnot_validated, and the API blockstools/beneficiary_otp_gate.py): the redeem call must be byte-identicalcorporate_reference), or it is refused| Parameter | Type | Required | Description |
|---|---|---|---|
display_name | string | yes | Payee display name (1–255 chars) |
currency_code | string | yes | ISO 4217; for crypto rails, the stablecoin code (USDC/USDT) |
payment_rail | string | yes | e.g. faster_payments, sepa, swift, eft, e_transfer, internal, pix, tron. Corridor legality is checked upstream |
entity_type | "individual" | "company" | unless rail internal | Payee entity type |
country_code | string | unless rail internal | ISO 3166-1 alpha-2 |
bank_account / e_transfer / virtual_account / sokin_internal / crypto | object | exactly one | The destination object matching the rail's payment method (rail→method mapping as in get_beneficiary_schema); inner keys are the camelCase names the schema returns |
individual / company | object | per entity_type | Identity details |
address | object | bank rails + e_transfer | Not required for internal, pix, or crypto rails |
corporate_reference | string | no | Corporate to create under. Required in practice; part of the byte-match |
otp_code | string | 2nd call | The 6-character code from the user's email |
confirm_verified_payee_name | boolean | no | Only after a VoP mismatch the user explicitly confirmed; changes the draft, so the flow restarts and needs a fresh code |
content / structuredContent — three shapes:isError false): {"status": "draft_invalid", "errors": [{"field", "message"}]} — text lists each field error and says nothing was created and no code email was sent.isError false): {"status": "otp_required", "expiresInSeconds": 600} — text instructs asking the user for the code and calling again with the same arguments plus otp_code. Any trigger while a code is outstanding for this user and action type (same draft or a different one) returns {"status": "otp_resent", ...} — text says a NEW code was emailed, any earlier code is now void, and to use the most recent email.isError true): any 5xx/transport error renders API error (N): <message> -- the request may still have gone through, so whether the payee was created is UNKNOWN. Do NOT retry blindly: check list_beneficiaries for this payee FIRST -- a blind retry can create a DUPLICATE. ... — the in-flight claim is kept, so an identical call is refused for up to 10 minutes.{"data": {...}} with externalReference, displayName, status, destinations[] (currencyCode, paymentRail, validationStatus), createdAt:Payee created: Acme Ltd (reference ben_new, status active).
Destinations: GBP via faster_payments (name verified).supportedRails and version are dropped as in get_beneficiary; amessage plussupportedRails, version, complete destinationsecurityAnswer, withheld even here); onError: display_name must not be empty. (and the same for currency_code / payment_rail)Error: payment_rail 'sokin_internal' is a payment METHOD, not a rail. ... — catches the method-vs-rail taxonomy confusionError: payment_rail 'faster_payments' takes exactly one destination object: bank_account (you supplied: e_transfer). Call get_beneficiary_schema for that corridor's field list.Error: entity_type and country_code are required for every payment rail except internal.Error: otp_code must be the 6-character code from the confirmation email (letters and digits). ...Error: these arguments don't match the draft that triggered the code (or more than 10 minutes have passed). ... — the byte-match refusalError: a newer confirmation code was requested after this draft's -- each new code email replaces the previous one of the same type, so the code for this draft is no longer redeemable. ... — the superseded-trigger refusalError: an identical create for this payee is already in flight. ... — on the trigger path this one can follow the dry-run pre-flight call; everything else above is purely localpaymentRail is treated as non-authoritative and theconfirm_verified_payee_name restart with a fresh code.API error (502) — whether the payee was created is unknown; checklist_beneficiaries before retrying, a blind retry can create a duplicate.update_beneficiaryPATCH of an existing payee's destination and identity details,create_beneficiary (samebeneficiary_id — same code mechanics anddisplayName, entityType, countryCode, andaddress are immutable upstream ("<field> is immutable. Create a new
beneficiary to use a different value.") — and the API only rejects themcreate_beneficiary.payment_rail and currency_codepayment_rail before the code is issuedpaymentRail is required when a beneficiary has multiple destinations.);currency_code would fail after the code is consumedcurrencyCode is required when a beneficiary has multiple destinations.)corporate_reference alsoCorporate not found on this routePATCH.| Parameter | Type | Required | Description |
|---|---|---|---|
beneficiary_id | string | yes | The payee's externalReference; part of the byte-match |
currency_code + payment_rail | string | multi-destination | Destination selectors, supplied together — not updates |
bank_account / e_transfer / virtual_account / sokin_internal / crypto / individual / company | object | at least one | Fields to change (camelCase inner keys) |
display_name / entity_type / country_code / address | — | refuse-only | Immutable upstream; accepted only to be refused up front, before any code is triggered or consumed |
corporate_reference | string | no | Required in practice; part of the byte-match |
otp_code | string | 2nd call | The 6-character code from the user's email |
confirm_verified_payee_name | boolean | no | VoP-mismatch override, same flow as create |
content / structuredContent — the otp_required / otp_resentcreate_beneficiary'sget_beneficiary). An identity patch is verifiable from the text:individual/company keys are echoed back as the record nowcompany.taxId, theentityType — which upstream silently drops with aIGNORED upstream; a successful update returns the full updatedget_beneficiary's shaping (envelope {"data": {...}},supportedRails/version/security answer dropped),| Currency | Rail | Validation | Account |
|---|---|---|---|
| GBP | faster_payments | matched | 87654321 |
message plus the raw recordsupportedRails, version, complete destination payloads — minus thesecurityAnswer, withheld even here).validate_beneficiary name check on the new bank details beforebeneficiary_id validationnot_validated, which blocks payments until refreshed.Error: beneficiary_id must not be empty.Error: display_name cannot be updated -- displayName, entityType, countryCode, and address are immutable on the API. ... — the refuse-only parameters, rejected before anything else runsError: bank_account must include at least one field to update -- omit the parameter entirely to leave it unchanged (a field cannot be cleared).Error: supply individual OR company, not both -- a payee has one immutable entity type, and the mismatched object would be silently ignored upstream after the code is consumed.Error: supply at least one field to update: a destination object (bank_account / e_transfer / virtual_account / sokin_internal / crypto), individual, or company. ... — also names the SELECTORS rule and the immutable fieldsError: supply payment_rail and currency_code together -- they jointly select which destination to update. ...create_beneficiary (create and update share the upstream action scopes, so a newer external-payee trigger of either tool supersedes a pending external draft of the other).create_beneficiaryAPI error (502) — whether the payee was updated is unknown; fetch it withget_beneficiary before retrying.list_financial_institutionscontent, a markdown table:| Id | Name | Bank Number |
|---|---|---|
| 1 | Royal Bank of Canada | 003 |
| 2 | Toronto-Dominion Bank | 004 |
structuredContent, envelope {"message": "Success.", "data": [...]},| Field | Type | Description |
|---|---|---|
id | integer | Institution id, a plain integer |
name | string | Institution name |
bankNumber | string | Institution's bank number |
list_financial_institution_branches| Parameter | Type | Required | Description |
|---|---|---|---|
financial_institution_id | integer or string | yes | The numeric Id from list_financial_institutions (not Bank Number, which is zero-padded and names a different bank). ASCII digits only, at most 9223372036854775807; surrounding whitespace is tolerated |
limit | integer | no | Max branches per page |
next_token | string | no | Cursor from the previous page |
content, a markdown table:| Id | Description | Routing Number | Address | City | State | Postal Code |
|---|---|---|---|---|---|---|
| 4412 | Main Branch | 000312345 | 200 Bay Street, Suite 400 | Toronto | ON | M5J2J2 |
structuredContent, envelope {"data": [...], "pagination": {...}},id, description, routingNumber, addressLineOne,addressLineTwo, city, state, postalCode.message field only.| Condition | Error |
|---|---|
| Non-numeric id (rejected locally before any upstream call) | Error: financial_institution_id must be a numeric id from list_financial_institutions (at most 9223372036854775807), got 'abc'. Call list_financial_institutions to see valid ids. |
| Unknown numeric id after an empty branches result | Unknown financial institution id: '999'. Call list_financial_institutions to see valid ids. |
get_documentation_links| Parameter | Type | Required | Description |
|---|---|---|---|
topic | "b2b_api", "mcp_server", or "all" | no | Which documentation to look up. Default "all" |
b2b_api: the Sokin B2B REST API this server calls: endpoints, auth,mcp_server: this MCP server itself: its tools, their returns, andall: every topic. The default, and the right choice when unsure.content, a header naming the deployment, a markdown table, then any| Documentation | URL | What's there |
|---|---|---|
| Sokin B2B API reference | https://docs.sokin.com/llms.txt | The Sokin B2B REST API this MCP server calls: every endpoint, authentication, API versions, and request/response schemas |
| Sokin MCP server reference | https://docs.sokin.com/sokin-mcp-10453423f0 | This MCP server: the tools it exposes, what each returns, and how to connect a client |
(none available) rather thansummary column carries the reason: the row isstructuredContent, flat, no data envelope:| Field | Type | Description |
|---|---|---|
mcpServerUrl | string | Public base URL of the deployment that answered, so UAT and production docs are distinguishable without inferring it from the URLs |
documentation | array | One record per requested topic, in a stable order |
documentation record has the same shape, so it can be iterated| Field | Type | Description |
|---|---|---|
topic | string | b2b_api or mcp_server |
title | string | Human-readable name of the reference |
url | string, or null | Where to read it. Null when nothing is configured for that topic |
alternateUrl | string, or null | A genuinely different second URL, when one exists; typically the human-readable page when url is an agent-oriented index |
summary | string | One line on what lives there, or on why there is no URL |
{
"mcpServerUrl": "https://mcp.uat.sokin.com",
"documentation": [
{
"topic": "b2b_api",
"title": "Sokin B2B API reference",
"url": "https://docs.sokin.com/llms.txt",
"alternateUrl": "https://docs.sokin.com",
"summary": "The Sokin B2B REST API this MCP server calls: every endpoint, authentication, API versions, and request/response schemas"
},
{
"topic": "mcp_server",
"title": "Sokin MCP server reference",
"url": "https://docs.sokin.com/sokin-mcp-10453423f0",
"alternateUrl": null,
"summary": "This MCP server: the tools it exposes, what each returns, and how to connect a client"
}
]
}b2b_api,url and the human page becomes alternateUrl. Thevisualizeui://sokin-mcp/visualize). It is a diagramming tool, not a| Parameter | Type | Required | Description |
|---|---|---|---|
markdown | string | yes | Full CommonMark + GFM. Fenced blocks tagged mermaid render as diagrams. Raw HTML is sanitized, not rendered |
title | string | no | The card's header. Defaults to a generic heading |
TD, not LR), keep labels short, and keep any%%{init:
...}%% theme directive; the card applies its own brand theme and yourscontent, the markdown string, echoed verbatim.structuredContent, flat, no envelope:| Field | Type | Description |
|---|---|---|
title | string | The title as passed |
markdown | string | The markdown as passed |
markdown must not be empty.play_cheetah_dashui://sokin-mcp/cheetah-dash). It takes no arguments and calls no Sokincontent, a plain string:Cheetah Dash is ready -- use Space or tap to jump, again mid-air to double jump.structuredContent: none. This is the only tool on this server thatstructuredContent at all.instructionType on list_instructions and get_instruction, the B2BInstructionType enum (8 values):| Value | Meaning |
|---|---|
Payment | Moves funds from a CCA to a beneficiary |
FX | Moves funds between two CCAs owned by the same corporate, in different currencies |
FXPayment | Moves funds from a CCA to a beneficiary in a different currency |
UnfundedPayment | Funds may land in the account after the instruction is created, before sending |
UnfundedFXPayment | The unfunded variant of FXPayment |
SokinDirect | Same-currency transfer between two on-platform CCAs, at zero fees |
BoostDeposit | Moves funds from a CCA, subject to treasury approval before completion |
ForwardSettlement | Settles a Forward Contract at the contracted forward rate |
displayStatus, the user-facing status, present in the compact response.Pending finalisation, Approval Initiated, Approval Completed,Pending Partner, Booking FX Trade, Pending, Pending FX Settlement,Pending Settlement, Processed, Failed, Unknown.get_instruction can also return Rejected -- not one of the 11 valuesFailed carries an "Approval rejected: " prefix onfailureReason, to tell a human's portal rejection apart from a realget_instruction's own section for the two fields this adds when it fires.status is dropped from the compact response and appears only when theCreated, ApprovalInitiated, ApprovalCompleted, Processed, Failed,PaymentInstructed, FXTradeBooked, OutboundComplianceCompleted,InboundComplianceCompleted, LiquidityConfirmed, FXTradeSettled,PaymentSettled, BoostApprovalRequested, BoostApprovalCompleted.displayStatus. These values track internal workflow transitionsstatus on get_instruction_request, the B2B API'sInstructionRequestStatus enum. Exactly 3 values, no more:Created, Accepted, Rejected.Created means stillAccepted means the instruction now exists, Rejected meanserrorCategory alongside it, and on validate_payment_instruction, isInstructionRequestErrorCategoryfees, amount, amount_missing_funds,corporate_currency_account, quote, beneficiary_reference,reseller_reference, reseller_fee, payment_details,corporate_reference. On the wire, though, both response models declarebeneficiary_currency is an eleventh value, but it does not come fromvalidate_payment_instruction andcreate_payment_instruction both synthesize it locally, before theget_by_reference_and_currencysokin-b2b-api-service conflates that case with "does not exist" andbeneficiary_reference for both). Same-currencycreate_fx_payment_instruction for a cross-currency paymentget_account_ledger entries:direction takes only two values in practice, credit or debititem.type.lower() of a Credit/Debit databasetype is not a short closed set, and this is the field most likely tosnake_case value. The full set you can receive (11 values):| Value | Notes |
|---|---|
deposit | Funds in |
withdrawal | Funds out |
FX Trade | Friendly label covering both fx_trade_in and fx_trade_out legs |
Forward settlement | Friendly label covering both forward_settlement_in and forward_settlement_out legs |
sokin_direct_send | On-platform CCA-to-CCA transfer, outgoing leg |
sokin_direct_receive | On-platform CCA-to-CCA transfer, incoming leg |
boost_deposit | Move into a Boost position |
boost_maturity_payout | Boost payout at maturity |
manual_adjustment_fraud_recall | Operational adjustment |
manual_adjustment_account_offboarding | Operational adjustment |
manual_adjustment_good_will | Operational adjustment |
deposit/withdrawal/FX Trade alone: thetype as displayable rather than an error.content table signs amounts from direction (+ for credit, - forstructuredContent.amount stays unsigned either way.localAccountStatus and internationalAccountStatus on get_account, theCCALocalityStatus enum. Exactly 4 values, no others:| Value | Meaning |
|---|---|
ACTIVE | The account is active and ready to receive payments |
PENDING | Not active yet; still processing, or awaiting operational action |
NOT_PROVISIONED | Can be set up, but account setup or terms acceptance is incomplete |
NOT_AVAILABLE | Pay-in over this route is unavailable for this account. Also the default value when the field is otherwise unset |
| Field | Values |
|---|---|
entityType | individual, company |
status | active, deleted |
paymentRail on a beneficiary destination, the B2B API's PaymentRail| Group | Values |
|---|---|
| Internal | internal |
| International | swift |
| Europe | sepa |
| United Kingdom | faster_payments, bacs, chaps |
| United States | ach, fedwire, fednow |
| Canada | eft, e_transfer |
| Asia-Pacific | fast_meps, npp |
| Middle East | fts_ipi |
| Latin America | pix |
| Crypto networks | base, tron, ethereum, polygon, solana |
assignmentType on list_corporates: direct or inherited.payment_purpose on validate_payment_instruction, create_payment_instruction,submit_payment_instruction, create_fx_payment_instruction, andsubmit_fx_payment_instruction is a declared schema enum (ARC-609), notfixed_side. When omittedACCOUNTS_PAYABLEPaymentPurpose.active_members(sokin_catalogue=True) incommon/enums.py):ACCOUNTS_PAYABLE, COMMISSION, CUSTOMER_REFUND, CUSTOMS_DUTIES,DIVIDEND_DISTRIBUTION, EXPENSE_REIMBURSEMENT, FREELANCER_CONTRACTOR,INSURANCE_PREMIUM, INTERCOMPANY_TRANSFER, LOAN_REPAYMENT,MARKETING_ADVERTISING, OFFICE_LEASE, OTHER, PROFESSIONAL_SERVICES,ROYALTY_LICENSE_FEE, SALARY_PAYROLL, SHIPPING_FREIGHT_LOGISTICS,SOFTWARE_SUBSCRIPTIONS, SUPPLIER_PAYMENT, TAX_PAYMENT,TRAVEL_EXPENSE, UTILITIES.SokinPaymentPurpose entirely (EXAP-290,GET /instruction-requests/payment-purposes, whichX-Api-Version header to decide which list to returnpayment_purpose will accept.PAYROLL_DEPOSIT,RESIDENTIAL_MORTGAGE, and COMMERCIAL_BILL_PAYMENT) client-side. This isX-Api-Version: 2026-09-01 instead of hardcoding).OTHER, is known to fail at the payout provider for a| Parameter | Tool | Values |
|---|---|---|
fixed_side | get_fx_quote, create_fx_quote, create_fx_payment_instruction | BUY, SELL (default SELL) |
search_mode | list_beneficiaries | contains, starts_with (default contains) |
topic | get_documentation_links | b2b_api, mcp_server, all (default all) |
payment_purpose | validate_payment_instruction, create_payment_instruction, submit_payment_instruction, create_fx_payment_instruction, submit_fx_payment_instruction | 22 values, see "Payment purposes" above |