| Field | Value |
|---|---|
| Endpoint | https://mcp.sokin.com/mcp |
| Transport | Streamable HTTP |
| Protocol | JSON-RPC 2.0 (MCP) |
| Authentication | OAuth 2.1 + PKCE; the client identifies itself with a Client ID Metadata Document (CIMD) |
| Tools | 26 |
| Session model | Stateless HTTP: no session ID, and any request can reach any replica. One thing is remembered per user and client: whether it declared interactive-card support at initialize (kept 24 hours, refreshed by each initialize) |
initialize first. A tools/call with no prior initialize isinitialize isio.modelcontextprotocol/ui extension under capabilities.extensions),initialize. With no declaration on record,create_fx_quote and settle_fx_quote refuse every call, get_fx_quoteget_fx_quote).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. Leave out the second value406 Not Acceptable ("Client must accept both| Endpoint | Purpose |
|---|---|
https://mcp.sokin.com/.well-known/oauth-protected-resource/mcp | Protected-resource metadata (RFC 9728): the resource identifier, https://mcp.sokin.com/mcp, and the authorization server that issues its tokens, https://auth.sokin.com/ |
https://auth.sokin.com/.well-known/oauth-authorization-server | The authorization server's metadata: its authorize and token endpoints, and client_id_metadata_document_supported: true |
/mcp is answered 401 with aWWW-Authenticate header whose resource_metadata points at the first/authorize, /token or /register.client_id is an HTTPS URL at which it publishes its own metadataregistration_endpoint, but registrationS256).resource parameter (RFC 8707) as https://mcp.sokin.com/mcp.aud includes that URL (it may be an array), and theoffline_access to receive a refresh/mcp are additionally gated by Host and Origin checks (DNS421, and one carrying a disallowed Origin with 403. IfHost 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.null. The Sokin APInull the same way. The explicit nulls are theidempotencyKey when a draftmessage 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 |
| 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, settle_fx_quote: 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 no structuredContentcontent and the flag. Branch on isError 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. Only list_corporates and list_financial_institutions, which take no reference, show the generic middle sentence. Exception: on a 404, validate_beneficiary, create_beneficiary and update_beneficiary name what is missing instead (Corporate not found or a missing payee id; see their entries), while a 403 still gets this neutral text. |
| 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. |
| Token exchange failed | API error (503): Could not obtain an API token for this session, please try again | Not the B2B API: the server could not exchange the client's token for a Sokin API token, because the authorization server was unreachable or refused. Retryable. The client's own token is never forwarded in its place. |
| 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_fx_payment_instruction can return content that begins withError: (its quote-problem message) while isError staysfalse: that is a reported finding from the validation dry run, not a toolidempotencyKey is null),502 through the same channel rather than rendered as a falsevalidate_payment_instruction does this when a 200 arrives withsubmit_payment_instruction,submit_fx_payment_instruction and settle_fx_quote do it when a 202externalReference.{
"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 parameterdraft object the two create_*previous_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 |
create_fx_payment_instruction | readOnlyHint: false, destructiveHint: false, idempotentHint: false | Prepares an FX payment for review. Mints a real quote on every preparing call (the same write as create_fx_quote); submits nothing and moves no money |
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_instruction and create_payment_instruction stay oncreate_fx_payment_instruction is a writecreate_beneficiary).create_payment_instruction/submit_payment_instructioncreate_fx_payment_instruction/submit_fx_payment_instruction) workinitialize. The human control for a payment is not the review card butsettle_fx_quote for its limits,create -> confirm in chat -> submit flow is therefore a supported path,create_fx_quote and settle_fx_quote refuse a session withoutsettle_fx_quote's entry below).create_* tools read the session's declared capability only tosubmit_*create_* tool renders its review card and itssubmit_* tool,submit_payment_instruction).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 takes raw pricing inputsbuy_currency, sell_currency, amount, fixed_side,corporate_reference) and mints its own quote, via the same FX quotecreate_fx_quote uses, so no separate quote call comes first. Thesubmit_fx_payment_instruction takes quote_id: it has to match that samecreate_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: see the Example below.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.{"name": "list_corporates", "arguments": {}}2 corporates: CORP-8842 — Northwind Trading Ltd (direct assignment); CORP-9110 — Northwind Logistics BV (inherited access).{
"data": {
"items": [
{"corporateReference": "CORP-8842", "corporateName": "Northwind Trading Ltd", "assignmentType": "direct"},
{"corporateReference": "CORP-9110", "corporateName": "Northwind Logistics BV", "assignmentType": "inherited"}
]
}
}list_accounts| Parameter | Type | Required | Description |
|---|---|---|---|
corporate_reference | string | yes | The corporateReference from list_corporates |
content, one sentence: see the Example below.availableBalance is nullable upstream: it is balance minus thebalance nor availableBalance includes Boost (Easy Access Boostbalance includes held funds, availableBalance excludesbalance; it is a portal figure, not part of this API, soget_account_ledger shows the movementboost_deposit entry with counterpartySokin Boost; money coming back from Boost -- an Easy Access withdrawal,boost_maturity_payout entry, which carries neither a counterparty nor astructuredContent, 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 |
{"name": "list_accounts", "arguments": {"corporate_reference": "CORP-8842"}}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.{
"message": "Success.",
"data": [
{
"corporateReference": "CORP-8842",
"corporateCurrencyAccountReference": "CCA-1001",
"currencyCode": "GBP",
"balance": "48200.00",
"availableBalance": "47150.00"
},
{
"corporateReference": "CORP-8842",
"corporateCurrencyAccountReference": "CCA-1002",
"currencyCode": "USD",
"balance": "12000.00",
"availableBalance": null
}
],
"pagination": {"nextToken": null, "previousToken": null, "totalItems": 2, "hasMore": false}
}get_account| Parameter | Type | Required | Description |
|---|---|---|---|
account_reference | string | yes | The corporate currency account reference |
content, a sentence per funding route: see the Example below.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 |
currentBalance nor availableBalance includes Boost (EasycurrentBalance includes held funds,availableBalance excludes them. The Sokin portal's "Total Funds" figurecurrentBalance; it is a portalget_account_ledger shows the movement itself: a Boost deposit is aboost_deposit entry with counterparty Sokin Boost; money coming backboost_maturity_payout entry, which carriesaccountName, accountNumber,accountType, iban, routingCodeType, routingCode, bicCode, email,corporateReference, bankName, bankCountry, bankAddress,bankBranch.version only, an optimistic-concurrency counteraccount_reference must not be blank. This is{"name": "get_account", "arguments": {"account_reference": "CCA-1001"}}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.{
"message": "Success.",
"data": {
"externalReference": "CCA-1001",
"currency": "GBP",
"currentBalance": "48200.00",
"availableBalance": "47150.00",
"localAccountStatus": "ACTIVE",
"internationalAccountStatus": "NOT_PROVISIONED",
"localPaymentDetails": {
"accountName": "Northwind Trading Ltd",
"accountNumber": "12345678",
"routingCodeType": "SortCode",
"routingCode": "04-00-75",
"bankName": "Modulr FS Limited",
"bankBranch": "London",
"bankCountry": "GB"
},
"internationalPaymentDetails": []
}
}get_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.{
"name": "get_account_ledger",
"arguments": {"account_reference": "CCA-1001", "from_date": "2026-08-01", "limit": 25}
}| 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 |
{
"message": "Success.",
"data": [
{
"amount": "5000.00",
"direction": "credit",
"type": "deposit",
"effectiveDate": "2026-08-14",
"sourceReference": "Contoso Ltd",
"beneficiary": null,
"description": "Invoice 4471",
"transactionReference": "TXN-77120"
},
{
"amount": "1250.00",
"direction": "debit",
"type": "withdrawal",
"effectiveDate": "2026-08-15",
"sourceReference": null,
"beneficiary": "Fabrikam GmbH",
"description": "Supplier payment",
"transactionReference": "TXN-77121"
},
{
"amount": "800.00",
"direction": "debit",
"type": "FX Trade",
"effectiveDate": "2026-08-16",
"sourceReference": null,
"beneficiary": null,
"description": "GBP to EUR conversion",
"transactionReference": "TXN-77122"
}
],
"pagination": {"nextToken": "eyJrIjoi...", "previousToken": null, "totalItems": 3, "hasMore": true}
}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 | Deprecated on the backend itself -- true for terminal (Processed or Failed), false for everything still in progress. Mutually exclusive with exclude_display_status, which should be preferred -- see below |
exclude_display_status | enum, repeatable | no | Exclude rows whose displayStatus is in this list. Only Processed and Failed are accepted, the backend's only terminal statuses; any other value is rejected by schema validation, not sent. Mutually exclusive with completed |
limit | integer | no | Max instructions (default 20, max 100) |
next_token | string | no | Cursor from the previous page |
sort_order | enum | no | "desc" (default, newest first) or "asc" (oldest first), by instruction id |
corporate_reference or cca_reference is required.completed cannot express "outstanding including failures".completed=true includes Failed as well as Processed, so askingcompleted=false for what is still outstanding silently excludes everyexclude_display_status=["Processed"] is what'sdisplayStatusexclude_display_status=["Processed", "Failed"] is narrower: still inProcessed and read eachdisplayStatus instead. This list also shows approvalFailed (only get_instruction relabels them Rejected),Failed drops those too, and Rejected is not an acceptedGET /instructions)completed deprecated in favour of excludeDisplayStatus, soexclude_display_status is the parameter to build on, not just the morecontent, a markdown table: see the Example below.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.Error: Provide only one of completed or exclude_display_status, not both.exclude_display_status value other than Processed or Failed fails MCP argument-schema validation before the tool body runs, the same as an invalid payment_purpose elsewhere in this reference.sort_order value other than asc or desc fails the same schema validation.{"name": "list_instructions", "arguments": {"corporate_reference": "CORP-8842", "limit": 20}}| 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 |
{
"message": "Success.",
"data": [
{
"instructionReference": "INS-5512",
"corporateReference": "CORP-8842",
"ccaReference": "CCA-1001",
"instructionType": "Payment",
"amount": "1250.00",
"currency": "GBP",
"displayStatus": "Processed",
"createdAt": "2026-08-15T09:12:44Z",
"updatedAt": "2026-08-15T09:14:02Z"
},
{
"instructionReference": "INS-5513",
"corporateReference": "CORP-8842",
"ccaReference": "CCA-1002",
"instructionType": "FXPayment",
"amount": "800.00",
"currency": "USD",
"displayStatus": "Pending Settlement",
"createdAt": "2026-08-16T11:03:02Z",
"updatedAt": "2026-08-16T11:04:18Z"
}
],
"pagination": {"nextToken": null, "previousToken": null, "totalItems": 2, "hasMore": false}
}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 thestructuredContent, 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|<tenant>|<principal-id>); 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 (see{"name": "get_instruction", "arguments": {"instruction_reference": "INS-5513"}}Instruction 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.{
"message": "Success.",
"data": {
"externalReference": "INS-5513",
"displayStatus": "Pending Settlement",
"instructionType": "FXPayment",
"corporateReference": "CORP-8842",
"corporateCurrencyAccount": "CCA-1002",
"amount": "800.00",
"destinationAmount": "742.10",
"sourceCurrency": "USD",
"destinationCurrency": "EUR",
"beneficiaryReference": "BEN-3301",
"senderReference": "INV-4471",
"paymentPurpose": "SUPPLIER_PAYMENT",
"sokinFxFee": "4.20",
"sokinTransactionFee": "1.50",
"resellerTransactionFee": "0.00",
"failureReason": null,
"instructionRequestReference": "IR-Payment-5513",
"createdAt": "2026-08-16T11:03:02Z",
"updatedAt": "2026-08-16T11:04:18Z"
}
}get_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 status{"name": "get_instruction_request", "arguments": {"instruction_request_reference": "IR-Payment-77"}}Instruction request IR-Payment-77 was accepted: instruction INS-9 was created. Use get_instruction for its full detail.{
"message": "Success.",
"data": {
"externalReference": "IR-Payment-77",
"status": "Accepted",
"createdInstructionReference": "INS-9",
"failureReason": null,
"errorCategory": null
}
}validate_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: see the Example below.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 success502:{
"name": "validate_payment_instruction",
"arguments": {
"corporate_currency_account": "CCA-1001",
"beneficiary_reference": "BEN-3301",
"amount": "1250.00",
"reseller_fee": "1.50",
"sender_reference": "INV-4471"
}
}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.{
"data": {
"success": true,
"failureReason": null,
"errorCategory": null,
"feeBreakdown": {
"amount": "1256.70",
"destinationAmount": "1250.00",
"sokinFee": "5.20",
"resellerFee": "1.50",
"feeRateCardReference": "RC-14"
}
}
}create_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 descriptionsubmit_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, whichsubmit_payment_instruction's destructivesubmit_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 acontent, 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 submitted within the duplicate window: 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, exceptreseller_fee, which becomes "" and is then refused as not a plainduplicateWarning is not an error. It means this exact payment hasinstructionRequestReference (waiting forsubmit_payment_instruction refuses the draft. The blocksender_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, the card treats it as "couldn'tsubmitted answer that carries noinstructionRequestReference, showing "couldn't check" instead. If notools/call), the card shows "couldn't check" too; a reply that arrives{
"name": "create_payment_instruction",
"arguments": {
"corporate_currency_account": "CCA-1001",
"beneficiary_reference": "BEN-3301",
"amount": "1250.00",
"reseller_fee": "1.50",
"sender_reference": "INV-4471"
}
}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_payment_instruction yourself.{
"draft": {
"corporate_currency_account": "CCA-1001",
"beneficiary_reference": "BEN-3301",
"amount": "1250.00",
"reseller_fee": "1.50",
"sender_reference": "INV-4471",
"payment_purpose": null,
"amount_is_fees_inclusive": false
},
"currency": "GBP",
"validation": {
"success": true,
"failureReason": null,
"errorCategory": null,
"feeBreakdown": {
"amount": "1256.70",
"destinationAmount": "1250.00",
"sokinFee": "5.20",
"resellerFee": "1.50",
"feeRateCardReference": "RC-14"
}
},
"idempotencyKey": "a5f1c8e0-7d3b-4f2a-9c61-0b8e4d2f1a77"
}submit_payment_instructionsubmit_fx_payment_instruction and settle_fx_quote). Thiscreate_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 "API errormessage 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 another key | Error: an identical payment was already submitted as IR-... at ... -- this submission was refused, no new payment was created. Lasts 12 hours. (Under the same key, the original's state is reported instead; see below) |
| 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.{
"name": "submit_payment_instruction",
"arguments": {
"idempotency_key": "a5f1c8e0-7d3b-4f2a-9c61-0b8e4d2f1a77",
"corporate_currency_account": "CCA-1001",
"beneficiary_reference": "BEN-3301",
"amount": "1250.00",
"reseller_fee": "1.50",
"sender_reference": "INV-4471"
}
}Payment submitted: instruction request IR-Payment-77 for 1250.00 from CCA-1001 to BEN-3301. 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.{"data": {"externalReference": "IR-Payment-77"}}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: see the Example below.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_quotesettle_fx_quote also 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 portal, 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 30 days. A settlement thatsettle_fx_quote keeps its claimquotedexpired with Get Quote, asettle_fx_quote records the attempt onsettling for those 30 days, 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, the card treats it as "couldn't check",unquoted. A store failure is an error result, which the cardcontent tells the modelOpen <url>.create_fx_quote refuses for the same reason. So a rendered card is notinitialize.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 copy{
"name": "get_fx_quote",
"arguments": {
"buy_currency": "USD",
"sell_currency": "GBP",
"amount": "1000.00",
"fixed_side": "SELL",
"corporate_reference": "CORP-8842",
"corporate_currency_account": "CCA-1001"
}
}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.corporateCurrencyAccountDisplay field below is the account's resolvedcorporate_currency_account is passed; if the account read fails itGBP account (number unavailable) and the card still renders.{
"buyCurrency": "USD",
"sellCurrency": "GBP",
"amount": "1000.00",
"fixedSide": "SELL",
"corporateReference": "CORP-8842",
"corporateCurrencyAccount": "CCA-1001",
"cardId": "0f8fad5b-d9cb-469f-a165-70867728950e",
"corporateCurrencyAccountDisplay": "GBP account 12345678"
}cardId is the card's own id. It is what lets the card remember its quote.This client cannot show the FX quote card, so no quote was created and no rate is shown. Open https://portal.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.{
"buyCurrency": "USD",
"sellCurrency": "GBP",
"amount": "1000.00",
"fixedSide": "SELL",
"corporateReference": "CORP-8842",
"indicative": true,
"quoteCreated": false,
"dashboardUrl": "https://portal.sokin.com/v2/transfer/currency-exchange"
}create_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.reseller_fee, if one is given, is added on top.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 portal 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 |
message field and thestatus (Pending, Created, Booked or Expired). The reverse direction matters too: idempotencyKey,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.{
"name": "create_fx_quote",
"arguments": {
"buy_currency": "USD",
"sell_currency": "GBP",
"amount": "1000.00",
"fixed_side": "SELL",
"corporate_reference": "CORP-8842",
"corporate_currency_account": "CCA-1001"
}
}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. Settling from GBP account 12345678.{
"quoteId": "QT-88213",
"idempotencyKey": "7c2e9b41-5a08-4d6f-b3c2-91ef0a4d8b52",
"currencyPair": "GBP/USD",
"fxRate": "1.18740",
"fixedSide": "SELL",
"buyAmount": "1187.40",
"buyCurrency": "USD",
"sellAmount": "1000.00",
"sellCurrency": "GBP",
"fxFees": "2.50",
"createdAt": "2026-08-27T14:32:00+00:00",
"expiresAt": "2026-08-27T14:34:00+00:00",
"corporateCurrencyAccount": "CCA-1001",
"corporateCurrencyAccountDisplay": "GBP account 12345678",
"resellerFee": null
}settle_fx_quotetools/list hides this tool from a client known to lack supportinitialize without declaring it), but shows it when support isinitialize or whoseError: this session has not declared MCP UI support, so real-money FX settlement is refused here. Direct the user to the Sokin portal to execute this trade instead.create_fx_quote, and nowhere else. It backs up portal approval (see "What| 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, andsettling after that, and create_fx_quotesettling 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 call returns the livecontent 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 |
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 portal 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 |
{
"name": "settle_fx_quote",
"arguments": {
"idempotency_key": "7c2e9b41-5a08-4d6f-b3c2-91ef0a4d8b52",
"corporate_currency_account": "CCA-1001",
"quote_id": "QT-88213"
}
}FX conversion submitted: instruction request IR-FX-9911 for account CCA-1001. This is a submission receipt, not confirmation the conversion completed -- check get_instruction_request for its actual outcome.{"data": {"externalReference": "IR-FX-9911"}}create_fx_payment_instructionPOST /instruction-requests/fx-payment on submit.create_payment_instruction, combining the account/beneficiary side of aquote_id. It takes the raw pricing inputsbuy_currency, sell_currency, amount, fixed_side,corporate_reference) and mints a real, live quote itself, via the same FXcreate_fx_quote uses (POST /fx/quotes/), with thebeneficiaryExternalReference. A quote minted by an earlier, separate callui/update-model-context), and the B2B API only checks a quote's existence,create_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: false, idempotentHint: false, like create_fx_quote, andcreate_payment_instruction's.Failed upstream with an Approval rejected: prefix, which this serverRejected) or refreshes the quote, which fails the originalfailureReason FX quote expired at approval (shown asFailed, not relabelled). Either way it arrives through the instruction'sFX conversion submitted by settle_fx_quote, whose quote is usually farcreate_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: see the Example below.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 toThe FX payment could not be validated: <reason>. Nothing has been submitted.errorCategory"quote"). Besides a quote that lapsed, this happens whencorporate_reference belongs to a different corporate than the account, orsell_currency is not the account's currency; preparing again with the sameError: 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 submitted within the duplicate window: 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 two{
"name": "create_fx_payment_instruction",
"arguments": {
"corporate_currency_account": "CCA-1001",
"beneficiary_reference": "BEN-3301",
"buy_currency": "USD",
"sell_currency": "GBP",
"amount": "100.00",
"fixed_side": "SELL",
"corporate_reference": "CORP-8842",
"sender_reference": "INV-4471"
}
}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.FX payment prepared and validated -- nothing has been submitted yet. Total debit 100.50 (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.{
"draft": {
"corporate_currency_account": "CCA-1001",
"beneficiary_reference": "BEN-3301",
"quote_id": "FXQ-1a2b3c",
"reseller_fee": null,
"sender_reference": "INV-4471",
"payment_purpose": null
},
"currency": "GBP",
"buyCurrency": "USD",
"quoteCreatedAt": "2026-09-16T10:02:00+00:00",
"quoteExpiresAt": "2026-09-16T10:12:00+00:00",
"idempotencyKey": "9c21ab4f-7c4e-4a2b-9f31-6d0e2a5b8c14",
"quoteRequest": {
"buy_currency": "USD",
"sell_currency": "GBP",
"amount": "100.00",
"fixed_side": "SELL",
"corporate_reference": "CORP-8842"
}
}submit_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 gateget_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,{
"name": "submit_fx_payment_instruction",
"arguments": {
"idempotency_key": "9c21ab4f-7c4e-4a2b-9f31-6d0e2a5b8c14",
"corporate_currency_account": "CCA-1001",
"beneficiary_reference": "BEN-3301",
"quote_id": "FXQ-1a2b3c",
"sender_reference": "INV-4471"
}
}FX payment submitted: instruction request IR-FXPayment-77 from CCA-1001 to BEN-3301. 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.{"data": {"externalReference": "IR-FXPayment-77"}}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: see the Example below.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.{
"name": "list_beneficiaries",
"arguments": {"corporate_reference": "CORP-8842", "search": "fab", "search_mode": "contains"}
}| 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 |
{
"data": [
{
"externalReference": "BEN-3301",
"displayName": "Fabrikam GmbH",
"entityType": "company",
"countryCode": "DE",
"destinations": [
{"currencyCode": "EUR", "paymentRail": "sepa"},
{"currencyCode": "EUR", "paymentRail": "swift"}
],
"status": "active"
},
{
"externalReference": "BEN-3302",
"displayName": "Jane Okafor",
"entityType": "individual",
"countryCode": "GB",
"destinations": [{"currencyCode": "GBP", "paymentRail": "faster_payments"}],
"status": "active"
}
],
"pagination": {"nextToken": null, "previousToken": null, "totalItems": 2, "hasMore": false}
}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. It does not tell an unknown corporate apart from an unknown payee: upstream 404s a corporate reference that does not exist but 403s one that exists and is foreign, so the distinction would answer "does that corporate exist".{"name": "get_beneficiary", "arguments": {"beneficiary_id": "BEN-3301", "corporate_reference": "CORP-8842"}}| Currency | Rail | Validation | Account |
|---|---|---|---|
| EUR | sepa | matched | DE89370400440532013000 |
| EUR | swift | not_validated | DE89370400440532013000 |
{
"data": {
"externalReference": "BEN-3301",
"displayName": "Fabrikam GmbH",
"entityType": "company",
"countryCode": "DE",
"status": "active",
"isInternal": false,
"destinations": [
{
"currencyCode": "EUR",
"paymentRail": "sepa",
"validationStatus": "matched",
"account_number": "DE89370400440532013000",
"institution": "Commerzbank AG"
},
{
"currencyCode": "EUR",
"paymentRail": "swift",
"validationStatus": "not_validated",
"account_number": "DE89370400440532013000",
"institution": "Commerzbank AG"
}
],
"company": {"registeredName": "Fabrikam GmbH"},
"address": {"city": "Berlin", "countryCode": "DE"},
"createdAt": "2026-02-11T09:20:00Z"
}
}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.{
"name": "get_beneficiary_schema",
"arguments": {
"payment_method": "bank_account",
"currency_code": "GBP",
"country_code": "GB",
"entity_type": "company",
"corporate_reference": "CORP-8842"
}
}| Field | Type | Required | Notes |
|---|---|---|---|
| displayName | string | yes | length 1-100 |
| bankAccount.accountNumber | string | only for faster_payments | |
| bankAccount.sortCode | string | yes | |
| entityType | enum | yes | one of: individual, company |
{
"data": {
"condition": {
"currencyCode": "GBP",
"entityType": "company",
"countryCode": "GB",
"paymentMethod": "bank_account"
},
"fields": [
{"name": "displayName", "fieldType": "string", "isRequired": true, "min": 1, "max": 100},
{
"name": "bankAccount.accountNumber",
"fieldType": "string",
"isRequired": true,
"requiredForPaymentRails": ["faster_payments"]
},
{"name": "bankAccount.sortCode", "fieldType": "string", "isRequired": true},
{
"name": "entityType",
"fieldType": "enum",
"isRequired": true,
"allowedValues": ["individual", "company"]
}
]
}
}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 ... — unlike get_beneficiary, this tool distinguishes a missing corporate from a missing payee id; its corporate branch 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 as{
"name": "validate_beneficiary",
"arguments": {
"sort_code": "04-00-75",
"account_number": "12345678",
"account_name": "Jane Okafor",
"corporate_reference": "CORP-8842"
}
}| Currency | Rail | Check | Status | Name on record | Details |
|---|---|---|---|---|---|
| GBP | faster_payments | COP | matched | Jane Okafor | Name matches the account holder |
{
"data": {
"isValid": true,
"results": [
{
"validationType": "COP",
"status": "matched",
"actualName": "Jane Okafor",
"details": "Name matches the account holder",
"currencyCode": "GBP",
"paymentRail": "faster_payments"
}
]
}
}create_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. Thisvalidate_beneficiary, and nothing stops a payee beingbeneficiary_id mode after creatingnot_validated, and the API blockscorporate_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.{
"name": "create_beneficiary",
"arguments": {
"display_name": "Contoso Ltd",
"currency_code": "GBP",
"payment_rail": "faster_payments",
"entity_type": "company",
"country_code": "GB",
"bank_account": {"accountNumber": "12345678", "sortCode": "040075"},
"company": {"registeredName": "Contoso Ltd"},
"address": {
"addressLineOne": "1 High St",
"city": "London",
"postCode": "EC1A 1AA",
"countryCode": "GB"
},
"corporate_reference": "CORP-8842"
}
}Payee created: Contoso Ltd (reference BEN-3310, status active).
Destinations: GBP via faster_payments (name verified).
No confirmation code was required for this corporate.{
"data": {
"externalReference": "BEN-3310",
"displayName": "Contoso Ltd",
"status": "active",
"createdAt": "2026-09-17T09:14:00Z",
"destinations": [
{
"currencyCode": "GBP",
"paymentRail": "faster_payments",
"validationStatus": "matched"
}
]
}
}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.{
"name": "update_beneficiary",
"arguments": {
"beneficiary_id": "BEN-3310",
"bank_account": {"accountNumber": "87654321"},
"corporate_reference": "CORP-8842",
"otp_code": "A1B2C3"
}
}otp_code is what triggers the email:A 6-character confirmation code has been emailed to the user. Ask them for it, then call update_beneficiary again with the SAME arguments plus otp_code. The code is single-use and expires in 10 minutes; to get a fresh one, call again without otp_code (max 5 emails per 15 minutes).| Currency | Rail | Validation | Account |
|---|---|---|---|
| GBP | faster_payments | matched | 87654321 |
{
"data": {
"externalReference": "BEN-3310",
"displayName": "Contoso Ltd",
"entityType": "company",
"countryCode": "GB",
"status": "active",
"createdAt": "2026-09-17T09:14:00Z",
"destinations": [
{
"currencyCode": "GBP",
"paymentRail": "faster_payments",
"validationStatus": "matched",
"account_number": "87654321"
}
]
}
}list_financial_institutionscontent, a markdown table: see the Example below.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 |
{"name": "list_financial_institutions", "arguments": {}}| Id | Name | Bank Number |
|---|---|---|
| 1 | Royal Bank of Canada | 003 |
| 2 | Toronto-Dominion Bank | 004 |
{
"message": "Success.",
"data": [
{"id": 1, "name": "Royal Bank of Canada", "bankNumber": "003"},
{"id": 2, "name": "Toronto-Dominion Bank", "bankNumber": "004"}
]
}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: see the Example below.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. |
{"name": "list_financial_institution_branches", "arguments": {"financial_institution_id": 1}}| Id | Description | Routing Number | Address | City | State | Postal Code |
|---|---|---|---|---|---|---|
| 4412 | Main Branch | 000312345 | 200 Bay Street, Suite 400 | Toronto | ON | M5J2J2 |
{
"data": [
{
"id": 4412,
"description": "Main Branch",
"routingNumber": "000312345",
"addressLineOne": "200 Bay Street, Suite 400",
"addressLineTwo": null,
"city": "Toronto",
"state": "ON",
"postalCode": "M5J2J2"
}
],
"pagination": {"nextToken": null, "previousToken": null, "totalItems": 1, "hasMore": false}
}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 server, a markdown table, then any(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 server that answered, so a client can tell which server the links describe 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 |
b2b_api,url and the human page becomes alternateUrl. The{"name": "get_documentation_links", "arguments": {"topic": "all"}}| 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 |
{
"mcpServerUrl": "https://mcp.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"
}
]
}visualizeui://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.{
"name": "visualize",
"arguments": {
"title": "Balances by currency",
"markdown": "```mermaid\npie title Balances\n \"GBP\" : 48200\n \"USD\" : 12000\n```"
}
}{"title": "Balances by currency", "markdown": "```mermaid\npie title Balances\n \"GBP\" : 48200\n \"USD\" : 12000\n```"}play_cheetah_dashui://sokin-mcp/cheetah-dash). It takes no arguments and calls no Sokincontent, a plain string: see the Example below.structuredContent: none. This is the only tool on this server thatstructuredContent at all.{"name": "play_cheetah_dash", "arguments": {}}Cheetah Dash is ready -- use Space or tap to jump, again mid-air to double jump.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 thebeneficiary_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 (14 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 |
manual_adjustment_fee_adjustment_refund | Operational adjustment |
manual_adjustment_other_manual_credit | Operational adjustment |
manual_adjustment_other_manual_debit | 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 PaymentRailsptr, spei,imps_neft, elixir), which come through under those names. pix_siloc_tedpix.| 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, not free text. Anfixed_side. When omitted entirely, the createACCOUNTS_PAYABLE server-side.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.PAYROLL_DEPOSIT).GET /instruction-requests/payment-purposes, returns either list dependingX-Api-Version header; it has no bearing on what theseOTHER, can fail at the payout provider for some routes, whose| 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 |
exclude_display_status | list_instructions | Processed, Failed (repeatable; mutually exclusive with completed) |
sort_order | list_instructions | desc, asc (default desc) |