V-RECReceive. Use. Connect.
API REFERENCE · TECHNICAL CONTRACT

V-REC API v1 / Online Learning & Token Pool / Merchant eKYC Preload / KYC R5.1 / Tenant Funding / Existing User Link / Crypto Wallet & Sales / Cards & Wallet Transactions v2.6.14

Implementation-oriented technical contract. Open each endpoint to inspect IN/OUT fields, types, required conditions, constraints, success responses, errors and security boundaries. Includes card PIN setup and issuer card transactions, clearly separated from the Merchant HMAC API.

MERCHANT SERVER API · ONLINE LEARNING / CUSTOM ERC-20

Online learning, Google Calendar & merchant token pool

These are merchant backend APIs, never browser-side secret-bearing APIs. Verify the logged-in learner on your backend; V-REC and SAKURA both enforce membership and merchant binding.

Production prerequisites: Requires an isolated merchant-to-SAKURA connection, operator-granted credential scopes and explicit Connected Apps reauthorization. Unisolated shared connections fail closed with 409.

MethodEndpointHMAC scopePurpose
GET/api/merchant/v1/learning/programslearning.readList up to 100 published courses belonging to this merchant
GET/api/merchant/v1/learning/programs/{id}learning.readCourse details and enrollment conditions (no secret URLs or quiz answers)
POST/api/merchant/v1/learning/programs/{id}/enrollmentslearning.enroll.writeEnroll consented member in free course only
GET/api/merchant/v1/learning/programs/{id}/sessionslearning.readGet lesson schedule and completion rules
GET/api/merchant/v1/learning/users/{id}/schedulelearning.readRead member course schedule
GET/api/merchant/v1/learning/users/{id}/progresslearning.readRead member attendance progress and Google Calendar add URLs
GET/api/merchant/v1/learning/rewardslearning.readRead recent custom token learning rewards
POST/api/merchant/v1/learning/sessions/{id}/completelearning.rewards.writeSubmit external lesson completion evidence
POST/api/merchant/v1/learning/sessions/{id}/rewardslearning.rewards.writeRetry configured reward for completed lesson
POST/api/merchant/v1/learning/users/{id}/calendar-synclearning.calendar.writeSync enrolled lessons to approved Google Calendar
GET/api/merchant/v1/token-poollearning.token_pool.readRead dedicated ERC-20 pool and all-time ledger statistics
GET/api/merchant/v1/token-pool/transferslearning.token_pool.readList pool transfers, statistics and cursor pagination
GET/api/merchant/v1/token-pool/transfers/{id}learning.token_pool.readRead a submitted transfer by request ID
POST/api/merchant/v1/token-pool/transferslearning.token_pool.writeRequest gasless ERC-20 transfer from dedicated merchant pool

See the expandable “Merchant Learning & Custom Token Pool API” cards below for all 14 endpoint inputs, outputs and errors. IN / OUT reference

Consent-based free course enrollment

POST /api/merchant/v1/learning/programs/331/enrollments
Content-Type: application/json
{"user_id":"usr_member_1234","learner_consent":true,"consent_reference":"consent-record-20261031-01"}

Both systems validate merchant membership. Explicit permission is required. Paid courses return 422 and must be purchased by the member.

External lesson completion evidence

POST /api/merchant/v1/learning/sessions/371/complete
Content-Type: application/json
{"user_id":"usr_member_1234","idempotency_key":"course-371-user-1234","evidence_ref":"provider-attendance-371-1234","duration_seconds":3600}

The lesson must have ended. SAKURA evaluates attendance, quiz and survey rules. HTTP 202 can mean pending; it is not token or blockchain confirmation. Reward amount is fixed by the configured lesson.

Google Calendar

GET progress supplies google_calendar_add_url for a user-clicked Google save screen. Automatic sync requires the user’s prior Google OAuth approval; missing authorization returns 409, partial failure may return 207.

GET  /api/merchant/v1/learning/users/usr_member_1234/progress
POST /api/merchant/v1/learning/users/usr_member_1234/calendar-sync
{"program_id":331}

Gasless transfer from a merchant pool

POST /api/merchant/v1/token-pool/transfers
{"merchant_code":"mrc_your_merchant_code","user_id":"usr_member_1234","request_id":"merchant-token-20261031-00001","amount":"10.500000"}
GET /api/merchant/v1/token-pool/transfers/merchant-token-20261031-00001

Requires ACTIVE/FULL membership, matching merchant, active ERC recipient wallet, dedicated custom-token backing and approved relayer. This is neither the USDT collateral pool nor a lesson reward. submitted is not confirmed; reconcile_required must not be automatically rebroadcast. Payout stays disabled until the relayer is verified.

External integration workflow · OpenAPI YAML · Merchant dashboard (login required)

MERCHANT API / EXTERNAL eKYC ONBOARDING

Merchant-driven onboarding: deliver only the eKYC link

Requires the explicit credential scopes kyc.preload and kyc.link.issue, which the Merchant grants in Merchant Portal → Merchant API after acknowledging its consent obligations (never included in a legacy "All" key). Standard users.write / kyc.read do not enable this flow. Completing the browser flow is not provider approval: wait until SAKURA/MatchMove reports APPROVED.

GET  /api/merchant/v1/profile-options
POST /api/merchant/v1/users             {"onboarding_mode":"ekyc_only", ...}
GET  /api/merchant/v1/users/{id}/kyc/schema
POST /api/merchant/v1/users/{id}/kyc/preload
     Idempotency-Key: unique-draft-request-id
     JSON: {"fields": {...}, "documents": {...}, "consent": {...}}
GET  /api/merchant/v1/users/{id}/kyc/preload-status
POST /api/merchant/v1/users/{id}/kyc/preload-reconcile  # only authorized correction
POST /api/merchant/v1/users/{id}/kyc/onboarding-link
     → ekyc_url (secure delivery by merchant to verified user)
GET  /api/merchant/v1/users/{id}/kyc  → APPROVED / REVIEW / PENDING / ...
Webhook: user.kyc.updated (optional)

2026-10-27: Explicit birthday/title/gender/nationality on every preload, applicants must be at least 18. Duplicate email/phone gives 409. Provider completion alone is not approval: final_review remains pending until SAKURA approval. Identity is locked after link issuance.

Every request uses existing Merchant HMAC headers. Base64 evidence is signed in JSON and forwarded to SAKURA as multipart. Never log or share eKYC bearer links with analytics services.

Flow guide · OpenAPI YAML

MERCHANT HMAC · CARD PRODUCT CATALOG

List available card product types (15-minute cache)

This is NOT an issued-cards endpoint. It returns MatchMove product codes, physical/virtual types, artwork, currencies, CVV metadata and issuer limits. Requires signed Merchant HMAC with cards.read and a connected SAKURA cards grant.

GET /api/merchant/v1/card-types
GET /api/merchant/v1/card-types?form_factor=physical
GET /api/merchant/v1/card-types?form_factor=virtual&code=seguammccard
Query parameterValue / validation
form_factorphysical / virtual (optional)
codeExact code; letters, digits, _ and -, max 100 characters

Response: data[] (id, code, name, description, form_factor, personalised_order, token, currency, image, details, created_at, updated_at), total, filters, meta. details may be an object or [] depending on provider data.

{"data":[{"id":1,"code":"seguammcpcard","name":"Sakura Exchange, Guam Inc. Physical Card","description":"Sakura Exchange, Guam Inc. Physical Card","form_factor":"physical","personalised_order":false,"token":{"type":"CVV","generation":"static"},"currency":["USD"],"image":{"large":"https://assets.mmvpay.com/global/products/seguammcpcard/card-large.png"},"details":{},"created_at":"2025-04-27 03:45:57","updated_at":"2026-10-29 02:30:15"}],"total":1,"filters":{"form_factor":"physical","code":null},"meta":{"source":"vrec_cache","stale":false,"refreshed_at":"2026-11-05T04:00:00+00:00","next_refresh_at":"2026-11-05T04:15:00+00:00","cache_ttl_seconds":900}}

Example JSON is illustrative. SAKURA refreshes the MatchMove catalog on a 900-second cycle and upserts mm_card_types; V-REC aligns its file-cache expiry to the SAKURA refresh deadline, so the delay does not accumulate. On provider failure, meta.stale=true indicates last-known data; 503 means no usable data. Catalog entries do not authorize card issuance.

HTTP: 200, 401, 403, 422, 429, 503. Cache-Control: private, no-store.

See full request/response contract below · OpenAPI YAML

MERCHANT HMAC · ATTACHED USER CARD INVENTORY

Read pre-existing SAKURA cards of an attached V-REC user

The default card list combines V-REC local cards with the attached identity’s existing personal Mastercard records in SAKURA. Requires cards.read, SAKURA cards grant, ACTIVE/FULL membership and can_view_balance=1. Cards issued to other V-REC programs and corporate Merchant-owned cards are excluded.

GET /api/merchant/v1/users/usr_example/cards
GET /api/merchant/v1/users/usr_example/cards?include_existing=false
GET /api/merchant/v1/users/usr_example/cards/skc_0123456789abcdef0123456789abcdef/transactions?from=2026-10-10&to=2026-11-09

data[].source indicates VREC_LOCAL / VREC_AND_SAKURA / SAKURA_ONLY; origin indicates VREC_LOCAL / VREC_ISSUED / SAKURA_EXISTING. SAKURA_ONLY cards have read-only skc_... IDs, which cannot be used for freeze, PIN, sensitive-card, or issuer-status APIs. Local card_... IDs are unchanged.

The card list does not include balances and is not a live issuer-status check. SAKURA may cap its inventory at 100 items (meta.sakura_truncated=true). SAKURA outages return 503, not a fabricated empty list. Use include_existing=false for the old V-REC-local-only response.

Card disclosure to a Merchant must have an appropriate legal basis and documented data-use purpose. Linking an identity does not automatically grant legal permission to disclose all separate financial activity.

OpenAPI YAML · Detailed API contract below

MERCHANT HMAC · ATTACHED USER CARD INVENTORY

Read pre-existing SAKURA cards of an attached V-REC user

The default card list combines V-REC local cards with the attached identity’s existing personal Mastercard records in SAKURA. Requires cards.read, SAKURA cards grant, ACTIVE/FULL membership and can_view_balance=1. Cards issued to other V-REC programs and corporate Merchant-owned cards are excluded.

GET /api/merchant/v1/users/usr_example/cards
GET /api/merchant/v1/users/usr_example/cards?include_existing=false
GET /api/merchant/v1/users/usr_example/cards/skc_0123456789abcdef0123456789abcdef/transactions?from=2026-10-10&to=2026-11-09

data[].source indicates VREC_LOCAL / VREC_AND_SAKURA / SAKURA_ONLY; origin indicates VREC_LOCAL / VREC_ISSUED / SAKURA_EXISTING. SAKURA_ONLY cards have read-only skc_... IDs, which cannot be used for freeze, PIN, sensitive-card, or issuer-status APIs. Local card_... IDs are unchanged.

The card list does not include balances and is not a live issuer-status check. SAKURA may cap its inventory at 100 items (meta.sakura_truncated=true). SAKURA outages return 503, not a fabricated empty list. Use include_existing=false for the old V-REC-local-only response.

Card disclosure to a Merchant must have an appropriate legal basis and documented data-use purpose. Linking an identity does not automatically grant legal permission to disclose all separate financial activity.

OpenAPI YAML · Detailed API contract below

MERCHANT HMAC · ATTACHED USER CARD INVENTORY

Read pre-existing SAKURA cards of an attached V-REC user

The default card list combines V-REC local cards with the attached identity’s existing personal Mastercard records in SAKURA. Requires cards.read, SAKURA cards grant, ACTIVE/FULL membership and can_view_balance=1. Cards issued to other V-REC programs and corporate Merchant-owned cards are excluded.

GET /api/merchant/v1/users/usr_example/cards
GET /api/merchant/v1/users/usr_example/cards?include_existing=false
GET /api/merchant/v1/users/usr_example/cards/skc_0123456789abcdef0123456789abcdef/transactions?from=2026-10-10&to=2026-11-09

data[].source indicates VREC_LOCAL / VREC_AND_SAKURA / SAKURA_ONLY; origin indicates VREC_LOCAL / VREC_ISSUED / SAKURA_EXISTING. SAKURA_ONLY cards have read-only skc_... IDs, which cannot be used for freeze, PIN, sensitive-card, or issuer-status APIs. Local card_... IDs are unchanged.

The card list does not include balances and is not a live issuer-status check. SAKURA may cap its inventory at 100 items (meta.sakura_truncated=true). SAKURA outages return 503, not a fabricated empty list. Use include_existing=false for the old V-REC-local-only response.

Card disclosure to a Merchant must have an appropriate legal basis and documented data-use purpose. Linking an identity does not automatically grant legal permission to disclose all separate financial activity.

OpenAPI YAML · Detailed API contract below

MERCHANT HMAC · TRANSACTION SOURCES

Which transaction feed should you use?

These three feeds have different data sources and scopes. Successful card issuance does not guarantee a card purchase. Never merge wallet events into card statements or represent issuer activity as Merchant ledger entries.

Data sourceMerchant HMAC GETWhat it returns
MatchMove wallet/api/merchant/v1/users/{id}/matchmove-wallet-transactionsWallet-level transfers, funding and related events; not filtered by card.
MatchMove card/api/merchant/v1/users/{id}/cards/{cardId}/transactionsIssuer transactions for exactly one card; from/to up to 90 days.
V-REC Merchant ledger/api/merchant/v1/users/{id}/transactionsMerchant-attributed balance changes; not complete issuer activity.

Authentication and access: All three use signed Merchant HMAC with transactions.read. Live MatchMove feeds require ACTIVE/FULL membership and can_view_balance=1; the wallet endpoint additionally requires SAKURA Connected Apps balances while the card endpoint requires cards.

GET /api/merchant/v1/users/usr_example/matchmove-wallet-transactions?page=1&limit=20
GET /api/merchant/v1/users/usr_example/cards/card_example/transactions?from=2026-10-05&to=2026-11-04&page=1&limit=20

Filtering live wallet history

Any filter activates cursor pagination: pass next_cursor as the next cursor with exactly the same filters and limit. Do not increment page. Without filters, page/limit behavior is unchanged.

ParameterPurpose
from / toIssuer dates inclusive; interval max 90 days.
category / typeNormalized category or exact MatchMove event type.
indicatordebit / credit
q / transaction_idText substring search or exact MatchMove transaction ID.
min_amount / max_amountInclusive wallet amount, separate from original JPY amount and fee.
transaction_mode / mccECOM/POS/ATM or merchant category code.
original_currency / currency / statusOriginal currency, wallet currency, provider status.
include_zero_amountIf false, hide zero-value events.
GET /api/merchant/v1/users/usr_example/matchmove-wallet-transactions?category=purchase&indicator=debit&from=2026-11-01&to=2026-11-04&limit=20

// next_cursorが返った場合(同じフィルターを繰り返す)
GET /api/merchant/v1/users/usr_example/matchmove-wallet-transactions?category=purchase&indicator=debit&from=2026-11-01&to=2026-11-04&limit=20&cursor=NEXT_CURSOR
Filtered total_records is null. Even if items is empty, search_complete=false and a next_cursor mean the search is still in progress. Issuer history availability is limited upstream.
Empty results versus outages: HTTP 200 items:[] can be a valid empty page. Upstream failures return HTTP 503 rather than fabricated zero results. A GET CARD wallet event is not necessarily a card payment.
Data disclosure boundary: A wallet-wide history may include events outside the requesting Merchant when a SAKURA identity is linked to multiple Merchants. Verify the user consent and the need-to-know boundary. SAKURA dashboard uses synced records whereas this new API fetches live records.

OpenAPI YAML · View full endpoint contracts below

FINANCIAL REPORTING · OPERATIONAL GUIDE

How Payout, Tenant Funding and Transactions differ

V-REC does not expose one fungible wallet. Every source has its own system of record. Keep transaction IDs and reconcile related events; never blindly sum different ledgers.

01 / USD SOURCE

Corporate Payout

Partner payout request. CREDITED indicates successful funding. A direct allocation is stored separately.

GET /api/v1/me/payouts
02 / USD SOURCE

Tenant Funding

Direct funding from tenant to user. A COMPLETED allocation creates an attributed lot and ledger CREDIT; returns are separate.

GET /api/v1/me/tenant-allocations
03 / SOURCE ACTIVITY

Transactions

V-REC ledger, local card entries, MatchMove issuer statements and SAKURA crypto events represent different records.

GET /api/v1/me/ledger-entries
Observed example: User 16 / instance 3 has one 10.00000000 USD completed Tenant Allocation and a corresponding ledger CREDIT, with zero payouts. The allocation and CREDIT are two representations of one funding event, not two deposits.
QuestionUse this APIInterpretation
Direct tenant funding receivedGET /api/v1/me/tenant-allocationsCOMPLETED / remaining_amount · USD
Value returned to tenantGET /api/v1/me/tenant-returnsCOMPLETED / PENDING · USD
Attributed ledger movementsGET /api/v1/me/ledger-entriesCREDIT / DEBIT / TENANT_RETURN
Corporate payoutsGET /api/v1/me/payoutsCREDITED ≠ direct Allocation
Internal card ledgerGET /api/v1/me/transactionsNot the full issuer statement
Actual card spending & ATMGET /api/v1/me/cards/{id}/issuer-transactionsMatchMove via SAKURA
USDT wallet transfersGET /api/v1/me/wallet/transfersUSDT / tx_hash
Purchases paid with cryptoGET /api/v1/me/wallet/purchasesPaid asset ≠ product delivered

How currencies and tokens are displayed

ActivityDebit / transferredReceivedLedger
Direct allocationTenant Fund · USD 10.00V-REC lot · USD 10.00Tenant Allocation + Ledger CREDIT
USDT Transfer12.345678 USDTExternal chain addressSAKURA wallet transfers
Buy SGP-USD with USDT10 USDTSGP-USD (quantity per contract)SAKURA sale contract + separate SGP-USD lot
Buy points with merchant token125 custom-token unitsMerchant points (per contract)SAKURA sale contract / Merchant point ledger
Crypto quantities above are illustrative display values, not exchange rates. USDT, merchant tokens, ETH, SGP-USD and USD are separate units. Use payment.amount with payment.symbol, and product_quantity with product_name; do not round or aggregate different assets.

Example: reconcile one direct allocation

Merchant HMAC: POST /api/merchant/v1/tenant-funding/allocations
  → status = COMPLETED, allocation.public_id = vta_...
Authenticated V-REC user: GET /api/v1/me/tenant-allocations
  → data[...].public_id = vta_..., amount = "10.00000000", currency = "USD"
Authenticated V-REC user: GET /api/v1/me/ledger-entries
  → entry_type = CREDIT, reference_type = SAKURA_TENANT_ALLOCATION
User payouts API: GET /api/v1/me/payouts
  → { "data": [] } (VALID: no corporate payout took place)
Authentication: /api/v1/me/* uses the V-REC user session; Merchant API uses HMAC. Issuer statements require the SAKURA cards scope, crypto histories require crypto_wallet. The new Merchant API reads live MatchMove wallet activity, but does not include the full internal SAKURA SGP-USD ledger.
Implementation-audited Spec 2.6.14 JSON / multipart / session

How to read this reference

Tap an API row to expand the complete contract for that endpoint. Required values marked conditional or dynamic depend on prior state or the current SAKURA KYCFORM definition.

Merchant HMAC

Every request is signed by the Merchant credential. Timestamp must be within ±300 seconds; nonce must be unique and 8–128 characters. Credentials are confined to their Merchant boundary.

Headers

X-VREC-Merchant-Key: <value>
X-VREC-Timestamp: <value>
X-VREC-Nonce: <value>
X-VREC-Signature: <value>

Canonical string

METHOD + "\n" + PATH + "\n" + CANONICAL_QUERY + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + SHA256(raw_body)

V-REC End-user Session

Same-origin V-REC user session. Unauthenticated requests are HTTP 302 redirects to /v/{instance}/login, not JSON 401. POST requires form field _csrf; mismatch is an HTTP 419 non-JSON body.

Merchant signature note: Sort query keys and RFC3986-encode CANONICAL_QUERY. SHA256 must be calculated over the exact raw body bytes sent on the wire.
v2.5.0Ethereum · USDT (ERC-20)End-user only

Unhosted wallet, rotation and gasless transfer

The wallet is SAKURA's unhosted ERC wallet. The 12-word BIP-39 seed phrase is returned exactly once after create/rotate, and the private key is encrypted with the user's wallet password. V-REC never stores the seed phrase, key or wallet password. For gasless transfers SAKURA's relayer pays the gas (ETH).

V-REC user (session)
  POST /api/v1/me/wallet            current_password step-up + wallet_password
        ↓ 201 + mnemonic (once)  → write down 12 words
  GET  /api/v1/me/wallet            address + USDT/ETH balances + gasless eligibility
  POST /api/v1/me/wallet/address-check   (address poisoning check)
  POST /api/v1/me/wallet/gasless-transfer  Idempotency-Key + wallet_password
        ↓ 201 SUBMITTED + tx_hash   |  202 transfer_status_unknown → GET /api/v1/me/wallet/requests/{key}
  POST /api/v1/me/wallet/rotate     step-up + new_wallet_password + reason → new mnemonic (once)

V-REC server → SAKURA Connected Apps (scopes crypto_wallet / crypto_wallet_manage)
  /api/integrations/vrec/users/{ref}/wallet*   Bearer + X-VRec-Proof, idempotency_key per write
Permissions: Enabled only when the merchant grants crypto_wallet (read) and crypto_wallet_manage (create, rotate, transfer) in SAKURA Connected Apps; re-authorize existing connections from the admin console. The Merchant API is read-only through wallets.read and can never operate a wallet.
Gasless eligibility: Available to SAKURA monthly subscribers (including merchant-managed and physical-card-exempt users), up to 5 attempts per day (failures count). Operator transfer blocks, wallet-password lockout and destination safety checks apply exactly as on SAKURA.
v2.6.0Sale contract (not an exchange)Company 1% + merchant royalty

Merchant-route purchases paid in crypto

Users purchase SGP-USD or merchant-defined points, paying with USDT or the merchant token. Sold under the SAKURA EXCHANGE licence; not a currency exchange. Each purchase fixes a contract number, seller, price and fees, then the user accepts and pays with the wallet password.

GoodsPaid inSellerPrice goes toMerchant revenue
SGP-USDUSDTSAKURA EXCHANGECompany USDT sales addressRoyalty (settled by the company)
Merchant pointsUSDTMerchantCompany USDT sales addressNet proceeds + royalty (settled by the company)
SGP-USD / pointsCustom tokenMerchant (sole responsibility)Merchant address (company never holds it)Keeps the royalty; goods backed by merchant USDT pool
GET  /api/v1/me/wallet/purchase-catalog
POST /api/v1/me/wallet/purchase-quotes          Idempotency-Key  → contract_no (QUOTED, 10 min)
POST /api/v1/me/wallet/purchases/{contract}/confirm  accept_terms + wallet_password → PAYMENT_SUBMITTED
     … 12 confirmations → COMPLETED (SGP-USD as merchant-route lot / points)
GET  /api/merchant/v1/crypto-sales               (Merchant HMAC, crypto_sales.read)
Different from the standard USD route: Company fee is 1% (standard 2%). Purchased SGP-USD is credited on SAKURA as a merchant-route lot, separate from standard purchases, and is never booked to the V-REC merchant-attributed balance (Tenant Funding).
BASE URL

Merchant Server API

https://api.v-rec.xyz
GET /api/merchant/v1/card-types List MatchMove card products (15-min cache) cards.read
Auth Merchant HMAC Scope cards.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Query parameters

FieldTypeRequiredConstraint / meaning
form_factorstringnophysical or virtualphysical|virtual
codestringnoExact card product code^[A-Za-z0-9_-]+$, maxLength=100

OUT · Response contract

FieldTypePresentConstraint / meaning
data[]array<object>yesCard product records including all stored metadatano card issuance authorization implied
data[].id / code / name / descriptioninteger / stringyesProduct ID, code, name and descriptioncode is the exact MatchMove product identifier
data[].form_factor / personalised_orderstring / booleanyesphysical/virtual and personalization flagform_factor=physical|virtual
data[].token / currency / image / detailsobject / array / object / object|arrayyesCVV configuration, currencies, image URLs and issuance limit metadataJSON decoded, not serialized strings; details can be []
data[].created_at / updated_atstring|nullyesDB creation and update timestampsYYYY-MM-DD HH:mm:ss when present
meta.source / meta.stale / meta.refreshed_at / meta.next_refresh_at / meta.cache_ttl_secondsstring / boolean / datetime|null / datetime / integeryesData source, stale flag, last MatchMove refresh time, refresh deadline and cache TTL900 seconds on SAKURA; stale=true means last-known data
total / filtersinteger / objectyesFiltered count and applied search parametersfilters.form_factor and filters.code are nullable

HTTP 200 · Expected response

{
    "data": [
        {
            "id": 1,
            "code": "seguammcpcard",
            "name": "Sakura Exchange, Guam Inc. Physical Card",
            "description": "Sakura Exchange, Guam Inc. Physical Card",
            "form_factor": "physical",
            "personalised_order": false,
            "token": {
                "type": "CVV",
                "generation": "static"
            },
            "currency": [
                "USD"
            ],
            "image": {
                "large": "https://assets.mmvpay.com/global/products/seguammcpcard/card-large.png"
            },
            "details": {
                "active_limits": {
                    "allowed": 1,
                    "pre_kyc": 0,
                    "post_kyc": 5
                }
            },
            "created_at": "2025-04-27 03:45:57",
            "updated_at": "2026-10-29 02:30:15"
        }
    ],
    "total": 1,
    "filters": {
        "form_factor": "physical",
        "code": null
    },
    "meta": {
        "source": "vrec_cache",
        "stale": false,
        "refreshed_at": "2026-11-05T04:00:00+00:00",
        "next_refresh_at": "2026-11-05T04:15:00+00:00",
        "cache_ttl_seconds": 900
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_signature_invalidFormat: JSON

HMAC authentication failed

HTTP 403merchant_scope_denied / sakura_cards_scope_requiredFormat: JSON

cards.read or SAKURA cards scope denied

HTTP 422invalid_form_factor / invalid_codeFormat: JSON

Invalid query filter

HTTP 503card_types_unavailable / card_types_upstream_unavailableFormat: JSON

Local database or upstream catalog unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • SAKURA refreshes MatchMove every 15 minutes. V-REC shares the refresh deadline. On failure last-known data are returned with meta.stale=true. Local V-REC DB is last-resort fallback. No issuance entitlement is implied.
GET /api/merchant/v1/context Get authenticated Merchant context users.read
Auth Merchant HMAC Scope users.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

OUT · Response contract

FieldTypePresentConstraint / meaning
merchant.idstringyesMerchant public IDmrc_...
merchant.namestringyesMerchant display name
merchant.program_idstringyesV-REC program public ID

HTTP 200 · Expected response

{
    "merchant": {
        "id": "mrc_demo",
        "name": "Example Merchant",
        "program_id": "vrec_demo"
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

GET /api/merchant/v1/profile-options Get authoritative SAKURA country/dial-code options users.read
Auth Merchant HMAC Scope users.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

OUT · Response contract

FieldTypePresentConstraint / meaning
countries[]array<object>yesCountry candidates allowed by SAKURA
countries[].codestringyesCountry code to submit^[A-Z]{2}$
countries[].provider_codestringyesCompatibility metadata; empty string when current SAKURA endpoint omits itdo not submit this instead of code
countries[].labelstringyesDisplay label
dial_codes[]array<object>yesDial-code candidates allowed by SAKURA
dial_codes[].codestringyesDial code to submit+digits
dial_codes[].provider_codestringyesCompatibility metadata; empty string when current SAKURA endpoint omits it
dial_codes[].labelstringyesDisplay label
generated_atstring|nullyesCatalog generation timestampISO 8601 when non-null

HTTP 200 · Expected response

{
    "countries": [
        {
            "code": "JP",
            "provider_code": "",
            "label": "Japan"
        }
    ],
    "dial_codes": [
        {
            "code": "+81",
            "provider_code": "",
            "label": "Japan (+81)"
        }
    ],
    "generated_at": "2026-10-05T01:00:00Z"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 503profile_options_unavailableFormat: JSON

SAKURA option catalog is unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Fail closed; do not fall back to free text when this catalog is unavailable.
GET /api/merchant/v1/users List users owned by the Merchant users.read
Auth Merchant HMAC Scope users.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Query parameters

FieldTypeRequiredConstraint / meaning
limitintegernoPage sizePHP (int) cast then clamp 1..100; omitted=>50; e.g. abc/0/negative=>1, >100=>100
offsetintegernoOffsetPHP (int) cast then max(0,...); omitted=>0; abc/negative=>0

OUT · Response contract

FieldTypePresentConstraint / meaning
data[]array<merchant_user>yesMerchant-owned user array
data[].idstringyesV-REC user public IDusers.public_id; usr_...
data[].beneficiary_idstringyesBeneficiary IDusers.beneficiary_id
data[].emailstringyesCurrent SAKURA email; V-REC stored email fallbackemail
data[].namestringyesCurrent SAKURA name; empty string if remote name is absent
data[].profileobjectyesCurrent SAKURA profile projection
data[].profile.first_namestringconditionalSAKURA canonical first nametrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
data[].profile.last_namestringconditionalSAKURA canonical last nametrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
data[].profile.country_codestringconditionalSAKURA canonical country codecurrent canonical projection; normally ISO alpha-2; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
data[].profile.dial_codestringconditionalSAKURA canonical dial codenormalized +digits; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
data[].profile.phonestringconditionalSAKURA canonical phonetrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
data[].profile.postal_codestringconditionalSAKURA canonical postal codetrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
data[].profile.address_line1stringconditionalSAKURA canonical address line 1trimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
data[].profile.address_line2stringconditionalSAKURA canonical address line 2trimmed string; key always present; may be empty; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
data[].profile.citystringconditionalSAKURA canonical citytrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
data[].profile.statestringconditionalSAKURA canonical state/prefecturetrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
data[].countrystring|nullyesprofile.country_code; null when absentISO alpha-2 when non-null
data[].kyc_statusstringyesCurrent SAKURA KYC status with V-REC fallbackNOT_STARTED|PENDING|APPROVED|REJECTED|REVIEW|CORRECTION_REQUIRED
data[].statusstringyesMembership status for this Merchantnormally ACTIVE|SUSPENDED; INACTIVE rows are excluded
data[].global_statusstringyesGlobal V-REC user status
data[].sakura_user_refstring|nullyesCurrent SAKURA user referencekey is always present; value may be null
data[].created_atstringyesV-REC creation timestampDB datetime
data[].updated_atstringyesV-REC update timestampDB datetime
limitintegeryesApplied limit1..100 after PHP integer-cast + clamp
offsetintegeryesApplied offset>=0 after PHP integer-cast + clamp

HTTP 200 · Expected response

{
    "data": [
        {
            "id": "usr_demo",
            "beneficiary_id": "VREC-DEMO",
            "email": "[email protected]",
            "name": "Taro Yamada",
            "profile": {
                "country_code": "JP"
            },
            "country": "JP",
            "kyc_status": "APPROVED",
            "status": "ACTIVE",
            "global_status": "ACTIVE",
            "sakura_user_ref": "USR_DEMO",
            "created_at": "2026-10-01 00:00:00",
            "updated_at": "2026-10-05 00:00:00"
        }
    ],
    "limit": 50,
    "offset": 0
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 503sakura_profile_unavailableFormat: JSON

SAKURA profile batch fetch failed

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

POST /api/merchant/v1/users Create or safely link a V-REC/SAKURA user users.write
Auth Merchant HMAC Scope users.write Content-Type application/json Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-KeystringyesCreation idempotency key1..190 chars

Request body

FieldTypeRequiredConstraint / meaning
emailstringyesEnd-user emailvalid email; SAKURA max 190
first_namestringyesFirst nametrimmed; non-empty; SAKURA max 120
last_namestringyesLast nametrimmed; non-empty; SAKURA max 120
country_codestringconditionalPreferred country code; must be countries[].coderequired unless legacy country is used; if key exists as empty string, legacy country is NOT used; SAKURA size 2
countrystringconditionalLegacy alias used only when country_code is missing/nullsame allowed catalog as country_code; do not send both
dial_codestringyesDial code from profile-optionsnormalized to +digits; SAKURA max 12
phonestringyesPhone numbertrim must be non-empty; SAKURA max 40
postal_codestringyesPostal codetrim must be non-empty; SAKURA max 40
statestringyesState/prefecturetrim must be non-empty; SAKURA max 120
citystringyesCitytrim must be non-empty; SAKURA max 120
address_line1stringyesAddress line 1trim must be non-empty; SAKURA max 190
address_line2stringyesAddress line 2trim must be non-empty in current V-REC flow; SAKURA max 190
notificationsobjectnoOnboarding email controlsomitted/null => both flags true; non-object => notifications_invalid; unknown keys ignored
notifications.send_vrec_activation_emailbooleannoSend V-REC activation emaildefault true; non-boolean when provided => notification_flag_invalid
notifications.send_sakura_password_setup_emailbooleannoSend SAKURA setup emaildefault true; non-boolean when provided => notification_flag_invalid
onboarding_modestringnoKYC-only registration with suppressed setup mailekyc_only only; no password returned

OUT · Response contract

FieldTypePresentConstraint / meaning
user.public_idstringyesV-REC user public IDusr_...
user.beneficiary_idstringyesBeneficiary ID
user.emailstringyesRegistered emailemail
user.namestringyesFull name
user.countrystring|nullyesSAKURA identity.profile.country_code; nullable in non-connected/mock branchISO alpha-2 when non-null
user.kyc_statusstringyesKYC status
user.statusstringyesMerchant membership status
user.global_statusstringyesGlobal user status
user.sakura_user_refstring|nullyesSAKURA user referencekey always present
user.sakura_customer_uuidstringyesSAKURA stable customer UUIDnon-empty or request fails
user.created_atstringyesCreated timestamp
user.activation_urlstringconditionalReturned only when a new local V-REC login is createdomitted for already-linked user; SAKURA password_setup_url is never returned
user.activation_expires_inintegerconditionalactivation_url lifetime in secondspresent with activation_url; 86400
user.notifications.vrec_activation_emailstringyesV-REC activation email resultsent|disabled|not_applicable|failed
user.notifications.sakura_password_setup_emailstringyesSAKURA password-setup email resultsent|disabled|not_applicable|failed

HTTP 201 · Expected response

{
    "user": {
        "public_id": "usr_demo",
        "beneficiary_id": "VREC-DEMO",
        "email": "[email protected]",
        "name": "Taro Yamada",
        "country": "JP",
        "kyc_status": "NOT_STARTED",
        "status": "ACTIVE",
        "global_status": "ACTIVE",
        "sakura_user_ref": "USR_DEMO",
        "sakura_customer_uuid": "cust_demo",
        "created_at": "2026-10-05 01:00:00",
        "activation_url": "https://v-rec.xyz/activate/token",
        "activation_expires_in": 86400,
        "notifications": {
            "vrec_activation_email": "sent",
            "sakura_password_setup_email": "sent"
        }
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 400invalid_jsonFormat: JSON

Malformed JSON body

HTTP 409idempotency_key_reused_with_different_request / request_in_progress / email_identity_mismatch / merchant_context_required / user_link_not_authorized / email_already_registeredFormat: JSON

Idempotency or identity conflict

HTTP 422idempotency_key_required / invalid_email / name_required / country_required / dial_code_required / phone_required / postal_code_required / state_required / city_required / address_line1_required / address_line2_required / invalid_country_code / invalid_dial_code / notifications_invalid / notification_flag_invalid / sakura_customer_uuid_missing / sakura_link_incompleteFormat: JSON

Input validation failure

HTTP 503upstream_or_internal_error / <SAKURA errorCode or sakura_unavailable>Format: JSON

SAKURA/upstream temporarily unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Same Idempotency-Key plus same raw body replays the stored response and does not resend onboarding email.
  • The SAKURA password_setup_url is a one-time bearer link and is never returned to Merchant API clients.
  • SAKURA length/MatchMove-preflight validation failures arrive as Connected App errors; current V-REC createUser maps every upstream status except 409 to HTTP 503.
  • country is a compatibility alias. If country_code exists and is non-null it wins; even an empty country_code prevents fallback to country.
  • Contract content type is application/json. Current controller does not enforce the Content-Type header, but json_decodes the raw body; the HMAC body hash covers the exact raw bytes.
  • ekyc_only suppresses both initial setup emails and removes the V-REC activation URL.
POST /api/merchant/v1/user-links Create existing SAKURA user consent link users.write
Auth Merchant HMAC Scope users.write Content-Type application/json Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-KeystringyesIdempotency key for the same link request1..190 chars; reuse only for identical external_customer_ref

Request body

FieldTypeRequiredConstraint / meaning
external_customer_refstring|nullnoMerchant customer reference. Never used to select the SAKURA identity.max 190

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 201 · Expected response

{
    "link_request": {
        "id": "ulr_demo",
        "status": "PENDING",
        "authorization_url": "https://payment.sakura-guam.com/vrec/user-link/vul_demo",
        "external_customer_ref": "customer-123",
        "expires_at": "2026-10-05T12:15:00Z",
        "linked_user_id": null,
        "activation_email_status": null
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

Merchant HMAC authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 409idempotency_key_required / idempotency_key_reused_with_different_request / sakura_not_connected / sakura_target_merchant_mismatch / sakura_link_request_mismatchFormat: JSON

Idempotency conflict or SAKURA connection not ready

HTTP 503link_request_unavailable / sakura_link_unavailableFormat: JSON

Temporary SAKURA/upstream failure

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • The Merchant cannot select a SAKURA identity by email or user id. Send authorization_url to the user; only the identity authenticated and approved by that user is linked.
  • V-REC binds the callback state, SAKURA link_request_id, Connected App target Merchant and exact V-REC Merchant. A code from another request or Merchant is rejected before membership creation.
GET /api/merchant/v1/user-links/{id} Get existing-user link request status users.read
Auth Merchant HMAC Scope users.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesLink request public IDulr_...

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "link_request": {
        "id": "ulr_demo",
        "status": "COMPLETED",
        "authorization_url": "https://payment.sakura-guam.com/vrec/user-link/vul_demo",
        "linked_user_id": "usr_demo",
        "activation_email_status": "sent",
        "completed_at": "2026-10-05T12:03:00Z",
        "membership": {
            "status": "ACTIVE",
            "access_mode": "READONLY",
            "source_merchant_id": "MRC_00000001",
            "can_receive_tenant_funds": false,
            "can_spend": false
        }
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

Merchant HMAC authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 404link_request_not_foundFormat: JSON

No such request inside this Merchant boundary

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Status is PENDING / COMPLETED / DENIED / EXPIRED etc. After COMPLETED, linked_user_id can be used with the normal User APIs.
GET /api/merchant/v1/users/{id} Get one Merchant-owned user users.read
Auth Merchant HMAC Scope users.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...

OUT · Response contract

FieldTypePresentConstraint / meaning
idstringyesV-REC user public IDusers.public_id; usr_...
beneficiary_idstringyesBeneficiary IDusers.beneficiary_id
emailstringyesCurrent SAKURA email; V-REC stored email fallbackemail
namestringyesCurrent SAKURA name; empty string if remote name is absent
profileobjectyesCurrent SAKURA profile projection
profile.first_namestringconditionalSAKURA canonical first nametrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.last_namestringconditionalSAKURA canonical last nametrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.country_codestringconditionalSAKURA canonical country codecurrent canonical projection; normally ISO alpha-2; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.dial_codestringconditionalSAKURA canonical dial codenormalized +digits; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.phonestringconditionalSAKURA canonical phonetrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.postal_codestringconditionalSAKURA canonical postal codetrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.address_line1stringconditionalSAKURA canonical address line 1trimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.address_line2stringconditionalSAKURA canonical address line 2trimmed string; key always present; may be empty; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.citystringconditionalSAKURA canonical citytrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.statestringconditionalSAKURA canonical state/prefecturetrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
countrystring|nullyesprofile.country_code; null when absentISO alpha-2 when non-null
kyc_statusstringyesCurrent SAKURA KYC status with V-REC fallbackNOT_STARTED|PENDING|APPROVED|REJECTED|REVIEW|CORRECTION_REQUIRED
statusstringyesMembership status for this Merchantnormally ACTIVE|SUSPENDED; INACTIVE rows are excluded
global_statusstringyesGlobal V-REC user status
sakura_user_refstring|nullyesCurrent SAKURA user referencekey is always present; value may be null
created_atstringyesV-REC creation timestampDB datetime
updated_atstringyesV-REC update timestampDB datetime

HTTP 200 · Expected response

{
    "id": "usr_demo",
    "beneficiary_id": "VREC-DEMO",
    "email": "[email protected]",
    "name": "Taro Yamada",
    "profile": {
        "country_code": "JP"
    },
    "country": "JP",
    "kyc_status": "APPROVED",
    "status": "ACTIVE",
    "global_status": "ACTIVE",
    "sakura_user_ref": "USR_DEMO",
    "created_at": "2026-10-01 00:00:00",
    "updated_at": "2026-10-05 00:00:00"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 404not_foundFormat: JSON

User does not exist within this Merchant boundary

HTTP 503sakura_profile_unavailableFormat: JSON

SAKURA profile unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

POST /api/merchant/v1/users/{id}/status Set Merchant membership ACTIVE/SUSPENDED users.write
Auth Merchant HMAC Scope users.write Content-Type application/json Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...

Request body

FieldTypeRequiredConstraint / meaning
statusstringyesMembership statusACTIVE|SUSPENDED; trim + strtoupper => case-insensitive input

OUT · Response contract

FieldTypePresentConstraint / meaning
idstringyesV-REC user public IDusers.public_id; usr_...
beneficiary_idstringyesBeneficiary IDusers.beneficiary_id
emailstringyesCurrent SAKURA email; V-REC stored email fallbackemail
namestringyesCurrent SAKURA name; empty string if remote name is absent
profileobjectyesCurrent SAKURA profile projection
profile.first_namestringconditionalSAKURA canonical first nametrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.last_namestringconditionalSAKURA canonical last nametrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.country_codestringconditionalSAKURA canonical country codecurrent canonical projection; normally ISO alpha-2; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.dial_codestringconditionalSAKURA canonical dial codenormalized +digits; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.phonestringconditionalSAKURA canonical phonetrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.postal_codestringconditionalSAKURA canonical postal codetrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.address_line1stringconditionalSAKURA canonical address line 1trimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.address_line2stringconditionalSAKURA canonical address line 2trimmed string; key always present; may be empty; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.citystringconditionalSAKURA canonical citytrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
profile.statestringconditionalSAKURA canonical state/prefecturetrimmed string; key always present; present when SAKURA profile projection includes this field; MerchantApiController may return profile={} when remote profile data is absent
countrystring|nullyesprofile.country_code; null when absentISO alpha-2 when non-null
kyc_statusstringyesCurrent SAKURA KYC status with V-REC fallbackNOT_STARTED|PENDING|APPROVED|REJECTED|REVIEW|CORRECTION_REQUIRED
statusstringyesMembership status for this Merchantnormally ACTIVE|SUSPENDED; INACTIVE rows are excluded
global_statusstringyesGlobal V-REC user status
sakura_user_refstring|nullyesCurrent SAKURA user referencekey is always present; value may be null
created_atstringyesV-REC creation timestampDB datetime
updated_atstringyesV-REC update timestampDB datetime

HTTP 200 · Expected response

{
    "id": "usr_demo",
    "beneficiary_id": "VREC-DEMO",
    "email": "[email protected]",
    "name": "Taro Yamada",
    "profile": {
        "country_code": "JP"
    },
    "country": "JP",
    "kyc_status": "APPROVED",
    "status": "SUSPENDED",
    "global_status": "ACTIVE",
    "sakura_user_ref": "USR_00000001",
    "created_at": "2026-10-01 00:00:00",
    "updated_at": "2026-10-05 00:00:00"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 400invalid_jsonFormat: JSON

Malformed JSON

HTTP 404not_foundFormat: JSON

User outside Merchant boundary

HTTP 422invalid_statusFormat: JSON

Status is not ACTIVE or SUSPENDED

HTTP 503sakura_profile_unavailableFormat: JSON

Post-update SAKURA profile unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Contract content type is application/json. Current controller does not enforce the Content-Type header, but json_decodes the raw body; the HMAC body hash covers the exact raw bytes.
GET /api/merchant/v1/users/{id}/kyc Synchronize and read KYC status from SAKURA kyc.read
Auth Merchant HMAC Scope kyc.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC user public IDusr_...
kyc_statusstringyesKYC status synchronized from SAKURANOT_STARTED|PENDING|APPROVED|REJECTED|REVIEW|CORRECTION_REQUIRED
checked_atstringyesLast sync timestamp updated by this requestDB UTC datetime

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "kyc_status": "APPROVED",
    "checked_at": "2026-10-05 01:00:00"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 404not_foundFormat: JSON

User outside Merchant boundary

HTTP 503kyc_sync_unavailableFormat: JSON

KYC synchronization unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Returns canonical SAKURA kyc_case. Provider completion without SAKURA approval is final_review / PENDING.
GET /api/merchant/v1/users/{id}/kyc/preload-status Read KYC status and owner kyc.preload
Auth Merchant HMAC Scope kyc.preload Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesMerchant-owned user ID

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "user_id": "usr_example",
    "stage": "final_review",
    "kyc_status": "PENDING",
    "next_action": {
        "type": "final_review"
    },
    "responsibility": {
        "owner": "SAKURA_REVIEWER"
    }
}

Expected errors / HTTP responses

HTTP 422validation_error

Field missing / applicant younger than 18

HTTP 409previous_preload_unresolved

Prior preload is unresolved

HTTP 503sakura_unavailable

SAKURA temporarily unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • GET never changes remote state. APPROVED is reported only after SAKURA final approval. final_review is owned by SAKURA_REVIEWER.
POST /api/merchant/v1/users/{id}/kyc/preload-reconcile Reconcile approved correction kyc.preload
Auth Merchant HMAC Scope kyc.preload Content-Type application/json Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesMerchant-owned user ID

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "user_id": "usr_example",
    "stage": "final_review",
    "kyc_status": "PENDING",
    "next_action": {
        "type": "final_review"
    },
    "responsibility": {
        "owner": "SAKURA_REVIEWER"
    }
}

Expected errors / HTTP responses

HTTP 422validation_error

Field missing / applicant younger than 18

HTTP 409previous_preload_unresolved

Prior preload is unresolved

HTTP 503sakura_unavailable

SAKURA temporarily unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Unlike the GET, this changes the SAKURA correction state, and only when the safety conditions are met. It cannot release a security hold.
POST /api/merchant/v1/users/{id}/kyc/start Get handoff information for end-user KYC kyc.start
Auth Merchant HMAC Scope kyc.start Success HTTP 202

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC user public ID
kyc_statusstringyesCurrent KYC status
actionstringyesAction required by merchantUSER_LOGIN_REQUIRED
user_urlstringyesV-REC URL where the end user authenticateshttps URL
messagestringyesImplementation message

HTTP 202 · Expected response

{
    "user_id": "usr_demo",
    "kyc_status": "NOT_STARTED",
    "action": "USER_LOGIN_REQUIRED",
    "user_url": "https://v-rec.xyz/v/program/login",
    "message": "The user must authenticate to V-REC before KYC can be started. No SAKURA impersonation URL is returned to merchant servers."
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 404not_foundFormat: JSON

User outside Merchant boundary

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • No eKYC short URL is returned. Only the authenticated end-user KYC flow may receive a redirect URL.
  • This endpoint does not parse a request body; kyc_status is the currently stored V-REC value and is not freshly synchronized here.
  • Contract content type is application/json. Current controller does not enforce the Content-Type header, but json_decodes the raw body; the HMAC body hash covers the exact raw bytes.
GET /api/merchant/v1/users/{id}/cards List pre-existing SAKURA and V-REC cards for an attached user cards.read
Auth Merchant HMAC Scope cards.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...

Query parameters

FieldTypeRequiredConstraint / meaning
include_existingbooleannoInclude existing SAKURA cardsdefault true; false/0 is V-REC local-only legacy mode; true/1 accepted

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC user public IDusers.public_id; current generated IDs use usr_...
data[]array<card_list_item>yesMasked card listPAN/CVV/PIN excluded
data[].idstringyesLocal V-REC card_... or read-only SAKURA inventory skc_... IDcard_... is local; skc_... is scoped to user/program and read-only
data[].card_typestringyesCard typeVIRTUAL|PHYSICAL
data[].brandstringyesCard brandMASTERCARD
data[].masked_panstring|nullyesMasked PANnever full PAN; nullable
data[].last4string|nullyesLast four PAN digitsnullable
data[].currencystring|nullyesCard currencySAKURA or local currency (3 letters), unknown null
data[].statusstringyesCard statusACTIVE|FROZEN|CLOSED (stored/provider-normalized)
data[].atm_enabledboolean|nullyesATM permission if known, otherwise nullunknown for SAKURA_ONLY; no false inference
data[].created_atstring|nullyesCard record creation timenull for card absent from V-REC local DB
metaobjectnoSAKURA inventory metadataonly include_existing=true; includes truncation status
data[].sourcestringyesRead sourceVREC_LOCAL | VREC_AND_SAKURA | SAKURA_ONLY
data[].originstringyesCard originationVREC_LOCAL | VREC_ISSUED | SAKURA_EXISTING
data[].read_onlybooleanyesRead-only identifiertrue for SAKURA_ONLY; no mutation permissions are ever inferred
data[].status_sourcestringyesStatus provenanceVREC_LOCAL_RECORD | SAKURA_CARD_RECORDS; NOT MatchMove live
meta.sakura_truncatedbooleannoSAKURA upstream 100-card truncationtrue means incomplete list
meta.inventory_completebooleannoSAKURA list not truncatedfalse when sakura_truncated=true
meta.sakura_is_livebooleannoNot a live issuer statealways false

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "data": [
        {
            "id": "card_demo",
            "card_type": "VIRTUAL",
            "brand": "MASTERCARD",
            "masked_pan": "**** **** **** 1234",
            "last4": "1234",
            "currency": "USD",
            "status": "ACTIVE",
            "atm_enabled": false,
            "created_at": "2026-10-01 00:00:00",
            "source": "VREC_AND_SAKURA",
            "origin": "VREC_ISSUED",
            "read_only": false,
            "status_source": "SAKURA_CARD_RECORDS"
        },
        {
            "id": "skc_0123456789abcdef0123456789abcdef",
            "card_type": "PHYSICAL",
            "brand": "MASTERCARD",
            "masked_pan": "**** **** **** 5678",
            "last4": "5678",
            "currency": "USD",
            "status": "ACTIVE",
            "atm_enabled": null,
            "created_at": null,
            "source": "SAKURA_ONLY",
            "origin": "SAKURA_EXISTING",
            "read_only": true,
            "status_source": "SAKURA_CARD_RECORDS"
        }
    ],
    "meta": {
        "includes_existing": true,
        "sakura_source": "SAKURA_CARD_RECORDS",
        "sakura_is_live": false,
        "sakura_truncated": false,
        "inventory_complete": true,
        "total_returned": 2
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_ip_not_allowedFormat: JSON

Merchant authentication, signature, timestamp, nonce or IP validation failed

HTTP 403merchant_scope_deniedFormat: JSON

Missing cards.read grant

HTTP 404not_foundFormat: JSON

User outside Merchant scope

HTTP 409cards_overview_requires_connected_apps / sakura_cards_scope_required / sakura_user_not_linkedFormat: JSON

SAKURA connection or user link missing

HTTP 422invalid_include_existingFormat: JSON

Invalid include_existing value

HTTP 429too_many_attemptsFormat: JSON

Inventory rate limit exceeded

HTTP 503sakura_card_inventory_unavailable / membership_capabilities_unavailableFormat: JSON

Upstream unavailable / capability migration incomplete, not zero cards

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • brand is the card network MASTERCARD, not the upstream card product name, for both PHYSICAL and VIRTUAL cards.
  • Full PAN, CVV and PIN are never returned.
  • Default include_existing=true. Merges saved SAKURA MMcard records with V-REC cards; NOT a live issuer-status query.
  • SAKURA_ONLY skc_... IDs work only for listing and issuer transaction GET. They cannot be used for status, PIN, sensitive-card, or issuer-status actions.
  • Upstream errors return 503, not zero cards. meta.sakura_truncated indicates a cap at 100 SAKURA cards.
  • Requires ACTIVE/FULL membership, can_view_balance=1, cards.read and SAKURA Connected Apps cards scope.
POST /api/merchant/v1/users/{id}/cards Issue VIRTUAL card by Merchant API cards.issue
Auth Merchant HMAC Scope cards.issue Content-Type application/json Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-KeystringyesIdempotency key8-190 chars; mandatory

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...

Request body

FieldTypeRequiredConstraint / meaning
card_typestringyesCard typeVIRTUAL only

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC user IDusr_...
cardobjectyesMasked issued cardno PAN/CVV
idempotent_replaybooleanyesPresent for idempotent replay onlytrue on replay

HTTP 201 · Expected response

{
    "user_id": "usr_demo",
    "card": {
        "id": "card_demo",
        "card_type": "VIRTUAL",
        "masked_pan": "**** **** **** 1234"
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_ip_not_allowedFormat: JSON

Merchant authentication, signature, timestamp, nonce or IP validation failed

HTTP 403merchant_scope_denied / full_membership_required / merchant_membership_capability_denied / user_not_activeFormat: JSON

Scope, membership, capability, or active-user requirement not met

HTTP 404not_foundFormat: JSON

User not found in Merchant scope

HTTP 409idempotency_key_reused_with_different_request / card_issue_reconciliation_required / card_issue_in_progressFormat: JSON

Idempotency conflict, unresolved issuer outcome or atomic-operation race

HTTP 422idempotency_key_required / only_virtual_card_type_supported / card_issue_in_progress / kyc_not_approved / card_issue_pre_submission_rejected / card_issue_confirmed_not_issuedFormat: JSON

Invalid request or definite pre-submission failure; a preflight card_issue_in_progress may return 422

HTTP 413request_too_largeFormat: JSON

Request body too large

HTTP 503card_issue_reconciliation_required / card_issue_operation_unavailableFormat: JSON

Uncertain issuer outcome or operation storage unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Operator grant required. Replays return the same card. Never reissue an uncertain request.
  • card_issue_in_progress can be 422 for a pre-submission lock guard or 409 for an atomic operation-claim race.
GET /api/merchant/v1/users/{id}/cards/issuance/{key} Read virtual-card issuance outcome (no issuer resubmission) cards.read
Auth Merchant HMAC Scope cards.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...
keystringyesIdempotency-Key used for issuance8–190 chars; A-Z a-z 0-9 . _ : -

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "status": "COMPLETED",
    "card_id": "card_demo",
    "reconciliation_required": false,
    "automatic_resubmission_allowed": false,
    "updated_at": "2026-10-23 00:00:00"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_ip_not_allowedFormat: JSON

Merchant authentication, signature, timestamp, nonce or IP validation failed

HTTP 403merchant_scope_denied / full_membership_required / merchant_membership_capability_denied / user_not_activeFormat: JSON

Missing read scope or active FULL membership

HTTP 404not_foundFormat: JSON

No matching issuance operation for Merchant and user

HTTP 422invalid_operation_keyFormat: JSON

Invalid operation key

HTTP 503membership_capabilities_unavailableFormat: JSON

Membership capability data unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Read-only: no issuer POST. RECONCILE_REQUIRED needs operator verification based on independent issuer evidence.
  • Statuses: PENDING, COMPLETED, RECONCILE_REQUIRED, REJECTED_PRE_SUBMISSION, NOT_ISSUED_CONFIRMED.
GET /api/merchant/v1/users/{id}/cards/{cardId}/transactions MatchMove card history for V-REC or existing SAKURA cards transactions.read
Auth Merchant HMAC Scope transactions.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...
cardIdstringyescard_... or skc_... from the card listcard_... V-REC local; skc_... SAKURA_ONLY read-only

Query parameters

FieldTypeRequiredConstraint / meaning
fromdatenoStart dateYYYY-MM-DD; default 30 days ago; maximum date span 90 days
todatenoEnd dateYYYY-MM-DD; default today; no later than UTC tomorrow
pageintegernoPagedefault 1; 1-100
limitintegernoLimitdefault 20; 1-50

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC public user IDusr_...
card_idstringyesV-REC public card IDcard_...
sourcestringyesIssuer card history sourcematchmove
pageintegeryesPage number1-100
limitintegeryesRequested page size1-50
fromdateyesStart dateYYYY-MM-DD
todateyesEnd dateYYYY-MM-DD; max span 90 days
totalinteger|nullyesIssuer total record count (null if unavailable)nullable
records_per_pageintegeryesIssuer page capacity actually usedused for next-page calculation
has_morebooleanyesMore pages may be availablebest effort
itemsarray<object>yesPer-card issuer transactions[] for valid no-transaction result
items[].idstringconditionalTransaction IDprovider value may be blank
items[].typestringconditionalCard transaction typeprovider value
items[].amountdecimal string|nullconditionalAmountnullable
items[].currencystringconditionalCurrencyprovider value
items[].descriptionstringconditionalDescriptionprovider value
items[].merchantstringconditionalMerchant nameprovider value
items[].statusstringconditionalTransaction statusprovider value
items[].datestringconditionalTransaction timestampprovider string
items[].indicatorstring|nullconditionalDebit/credit indicatornullable

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "card_id": "card_demo",
    "items": [
        {
            "id": "EXAMPLE_CARD_TX",
            "type": "PURCHASE",
            "amount": "12.50",
            "currency": "USD",
            "description": "Example Store",
            "merchant": "Example Store",
            "status": "APPROVED",
            "date": "2026-11-04T01:00:00Z",
            "indicator": "debit"
        }
    ],
    "page": 1,
    "limit": 20,
    "from": "2026-10-05",
    "to": "2026-11-04",
    "total": 1,
    "records_per_page": 20,
    "has_more": false,
    "source": "matchmove"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_ip_not_allowedFormat: JSON

Merchant authentication, signature, timestamp, nonce or IP validation failed

HTTP 403merchant_scope_deniedFormat: JSON

No permission

HTTP 404card_not_foundFormat: JSON

Card not found

HTTP 422invalid_filtersFormat: JSON

Invalid filter

HTTP 429too_many_attemptsFormat: JSON

Rate limited

HTTP 503issuer_transactions_unavailableFormat: JSON

Issuer unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • This is an issuer transaction feed from MatchMove, not local Merchant ledger entries.
  • Supply the V-REC card_... public identifier, not the raw MatchMove card ID. Requires SAKURA cards grant, ACTIVE/FULL Merchant membership and balance-view permission.
  • Date range is limited to 90 days. items[].date, merchant and indicator are normalized from MatchMove.
  • An empty 200 items array may be legitimate; issuing a card does not guarantee card purchases. Upstream failures return 503.
  • Card activity and MatchMove wallet history are different feeds. Use /matchmove-wallet-transactions for wallet activity.
  • skc_... is re-verified against program-scoped SAKURA ownership on each GET; raw MatchMove identifiers cannot be supplied.
GET /api/merchant/v1/users/{id}/matchmove-wallet-transactions Live MatchMove wallet transaction history (not per-card) transactions.read
Auth Merchant HMAC Scope transactions.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC public user IDusr_...

Query parameters

FieldTypeRequiredConstraint / meaning
pageintegernoNormal page; absent or 1 with search filters1-100; default 1; filtered search only page=1
limitintegernoPage size1-50; default 20
cursorstringnoOpaque next_cursor. Repeat all search filtersmax 512; search mode only
fromdatenoInclusive issuer date startYYYY-MM-DD; from/to span <= 90d
todatenoInclusive issuer date endYYYY-MM-DD; from/to span <= 90d
categorystringnoNormalized transaction categorypurchase|topup|transfer|atm|fee|authorization|card_issuance|adjustment|other
typestringnoExact MatchMove type, case-insensitivefree string max 80; e.g. Purchase
indicatorstringnoWallet credit/debit directioncredit|debit
qstringnoCase-insensitive merchant/description/type/id substringmax 120
min_amountdecimal stringnoInclusive gross wallet amount lower boundmax 8 decimals
max_amountdecimal stringnoInclusive gross wallet amount upper boundmax 8 decimals
currencystringnoWallet currencyISO 4217 / USD
original_currencystringnoOriginal transaction currencyISO 4217 / JPY
statusstringnoExact provider statuse.g. Success
transaction_modestringnoCard transaction modeecom|pos|atm
mccstringnoMerchant category codefour digits
transaction_idstringnoExact MatchMove transaction IDmax 190
include_zero_amountbooleannoIf false exclude 0-value transactionstrue|false; default true

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC public user IDusr_...
sourcestringyesLive MatchMove wallet sourceMATCHMOVE_WALLET_LIVE
observed_atISO 8601 stringyesFetch timestamp from SAKURAUTC
pageintegeryesPage number1-100
limitintegeryesRequested page size1-50
total_recordsinteger|nullyesUpstream record count; null when unknownnullable
has_morebooleanyesWhether a further page may existbest-effort if upstream count unknown
itemsarray<object>yesMatchMove wallet activity; empty only for valid zero-record responseno card ID filter
items[].idstring|nullconditionalMatchMove transaction identifiernullable
items[].transaction_datestring|nullconditionalTransaction timestampnullable; provider string
items[].typestring|nullconditionalEvent type (e.g. GET CARD)provider-specific
items[].descriptionstring|nullconditionalMerchant or activity descriptionnullable
items[].indicatorstring|nullconditionalDebit/credit indicatordebit / credit / null
items[].amountdecimal string|nullconditionalTransaction amountnullable; do not float-round
items[].currencystring|nullconditionalCurrency codenullable; do not assume USD
items[].statusstring|nullconditionalMatchMove transaction statusprovider-specific
pagination_modestringconditionalAdditional cursor search metadataconditional
next_cursorstring|nullconditionalAdditional cursor search metadatanullable in filter mode
search_completebooleanconditionalAdditional cursor search metadataconditional
scanned_recordsintegerconditionalAdditional cursor search metadataconditional
filtersobjectconditionalAdditional cursor search metadatanullable in filter mode
items[].categorystringconditionalSafe additional MatchMove field, null when absentconditional
items[].balancestring|nullconditionalSafe additional MatchMove field, null when absentconditional
items[].merchant_namestring|nullconditionalSafe additional MatchMove field, null when absentconditional
items[].merchant_category_codestring|nullconditionalSafe additional MatchMove field, null when absentconditional
items[].merchant_countrystring|nullconditionalSafe additional MatchMove field, null when absentconditional
items[].transaction_modestring|nullconditionalSafe additional MatchMove field, null when absentconditional
items[].original_amountstring|nullconditionalSafe additional MatchMove field, null when absentconditional
items[].original_currencystring|nullconditionalSafe additional MatchMove field, null when absentconditional
items[].fee_amountstring|nullconditionalSafe additional MatchMove field, null when absentconditional
items[].fee_currencystring|nullconditionalSafe additional MatchMove field, null when absentconditional

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "source": "MATCHMOVE_WALLET_LIVE",
    "observed_at": "2026-11-04T08:00:00Z",
    "page": 1,
    "limit": 20,
    "total_records": 1,
    "has_more": false,
    "items": [
        {
            "id": "EXAMPLE_TRANSACTION_ID",
            "transaction_date": "2026-11-04T01:00:00Z",
            "type": "GET CARD",
            "description": "PP Consumer",
            "indicator": "debit",
            "amount": "0.00",
            "currency": "USD",
            "status": "SUCCESS",
            "category": "card_issuance",
            "balance": "0.00",
            "merchant_name": null
        }
    ]
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_ip_not_allowedFormat: JSON

Invalid Merchant HMAC credentials, signature, timestamp or nonce

HTTP 403merchant_scope_denied / user_not_active / full_membership_required / merchant_membership_capability_deniedFormat: JSON

No transactions.read, ACTIVE/FULL membership, or balance-view permission

HTTP 404not_foundFormat: JSON

User not available to this Merchant

HTTP 409matchmove_wallet_not_linked / matchmove_wallet_identity_conflict / sakura_balances_scope_requiredFormat: JSON

MatchMove wallet missing / identity mismatch / SAKURA balances scope not granted

HTTP 422invalid_filtersFormat: JSON

Invalid filters, date range, cursor or pagination mode

HTTP 429too_many_attemptsFormat: JSON

30 requests per minute per Merchant/user

HTTP 503wallet_transactions_unavailable / membership_capabilities_unavailableFormat: JSON

SAKURA/MatchMove upstream unavailable; never represent as zero results

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Requires SAKURA Connected Apps balances, ACTIVE/FULL Merchant membership and can_view_balance=1.
  • Wallet-wide MatchMove records. Related to SAKURA dashboard MatchMove Transactions, but dashboard is locally synced while this endpoint calls the provider live.
  • This is not a per-card statement. A zero-value GET CARD event is not evidence of a card purchase.
  • For users linked to multiple Merchants, this wallet-wide feed may expose activity beyond the requesting Merchant. Confirm customer consent and disclosure permissions.
  • Response example is illustrative and does not imply a transaction occurred.
  • Any filter activates signed-cursor mode; up to 100 provider records are scanned per call. Continue next_cursor until search_complete=true. An empty interim items array does not mean zero matches.
  • Filtered total_records=null. Repeat identical filters and limit with next_cursor, never increment page.
  • Filter searches only the history actually available live from MatchMove, which documents a roughly 90-day retrieval limit. Locally archived older rows are not silently substituted.
POST /api/merchant/v1/users/{id}/cards/{cardId}/status Freeze/suspend or unfreeze an owned card cards.manage
Auth Merchant HMAC Scope cards.manage Content-Type application/json Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user idusr_...
cardIdstringyesV-REC card idcard_...

Request body

FieldTypeRequiredConstraint / meaning
statusstringyesFREEZE or SUSPEND becomes FROZEN; UNFREEZE becomes ACTIVEACTIVE|ACTIVATE|UNFREEZE|FROZEN|FREEZE|SUSPEND|SUSPENDED

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC user IDusr_...
card.idstringyesCard public IDcard_...
card.statusstringyesVerified card statusACTIVE or FROZEN
effective_statusstringyesRequested issuer-effective statusACTIVE or FROZEN

HTTP 200 · Expected response

{
    "user_id": "usr_example",
    "card": {
        "id": "card_example",
        "status": "FROZEN"
    },
    "effective_status": "FROZEN"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_ip_not_allowedFormat: JSON

Merchant authentication, signature, timestamp, nonce or IP validation failed

HTTP 403merchant_scope_deniedFormat: JSON

No permission

HTTP 404card_not_foundFormat: JSON

Card not found

HTTP 409closed_card_cannot_reactivate / card_status_update_rejected / active_merchant_membership_requiredFormat: JSON

Definitive local refusal such as a closed card; ambiguous issuer outcomes return 503

HTTP 422unsupported_card_statusFormat: JSON

Unsupported status

HTTP 429too_many_attemptsFormat: JSON

Rate limited

HTTP 503card_status_result_unconfirmedFormat: JSON

Issuer status unconfirmed

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Requires FULL membership, can_spend and explicit cards.manage. SUSPEND aliases issuer FROZEN; terminal close unsupported. Merchant cannot read PAN/CVV, PIN or password.
POST /api/merchant/v1/users/{id}/cards/{cardId}/sensitive-link Create hosted cardholder password challenge cards.sensitive.link
Auth Merchant HMAC Scope cards.sensitive.link Content-Type application/json Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...
cardIdstringyesV-REC card public IDcard_...

OUT · Response contract

FieldTypePresentConstraint / meaning
urlstringyesHosted V-REC password confirmation URLHTTPS; one-time; expires in 300s
password_requiredbooleanyesV-REC password requiredtrue
expires_in_secondsintegeryesLink expiry seconds300
displaystringyesDisplay modeHOSTED_VREC_ONLY

HTTP 200 · Expected response

{
    "url": "https://v-rec.example/card-reveal#one-time-token",
    "expires_in_seconds": 300,
    "password_required": true,
    "display": "HOSTED_VREC_ONLY"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_ip_not_allowedFormat: JSON

Merchant authentication, signature, timestamp, nonce or IP validation failed

HTTP 403merchant_scope_denied / full_membership_required / merchant_membership_capability_denied / user_not_activeFormat: JSON

No scope, active FULL membership or spending capability

HTTP 404not_found / card_not_foundFormat: JSON

User or card not found

HTTP 409card_not_activeFormat: JSON

Card status is not ACTIVE

HTTP 422empty_body_requiredFormat: JSON

Only empty body or {} is permitted

HTTP 429too_many_attemptsFormat: JSON

User challenge issuance rate exceeded

HTTP 503membership_capabilities_unavailable / secure_public_url_not_configuredFormat: JSON

Membership capability data unavailable or HTTPS public origin not configured

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Merchant never receives PAN/CVV. Password is entered on V-REC origin; one-use 5-minute challenge. Operator grant and cardholder V-REC password required.
  • Password re-authentication: 3 links and 8 password attempts per user per 24 hours across merchants.
GET /api/merchant/v1/users/{id}/matchmove-balance Query live MatchMove wallet available balance balances.read
Auth Merchant HMAC Scope balances.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesUser ID
sourcestringyesSourceMATCHMOVE_LIVE
currencystringyesCurrencyISO 4217
availabledecimal stringyesIssuer available balanceMatchMove direct
observed_atISO 8601yesObserved timeUTC
is_livebooleanyesLive provider requesttrue

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "source": "MATCHMOVE_LIVE",
    "currency": "USD",
    "available": "123.45",
    "observed_at": "2026-10-22T12:00:00Z",
    "is_live": true
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_ip_not_allowedFormat: JSON

Merchant authentication, signature, timestamp, nonce or IP validation failed

HTTP 403merchant_scope_deniedFormat: JSON

No permission

HTTP 503matchmove_balance_unavailableFormat: JSON

Issuer balance unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Unlike /balance this queries MatchMove over HTTP through SAKURA. Returns 503, not cached ledger amounts, when unavailable.
  • Internal SAKURA user_ref is omitted from Merchant responses.
POST /api/merchant/v1/users/{id}/cards/physical/link Link a pre-issued physical card by Proxy/Association ID cards.write
Auth Merchant HMAC Scope cards.write Content-Type application/json Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-KeystringyesLink idempotency keytrimmed non-empty, <=190; missing OR >190 both return idempotency_key_required

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...

Request body

FieldTypeRequiredConstraint / meaning
assoc_numberstringyesPhysical card Proxy / Association IDtrim -> remove ALL whitespace -> uppercase; then ^(?:PY)?\d{6,32}$; provider value normalized to PY + digits. Therefore raw input containing spaces may still be accepted.

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC user public IDusers.public_id; current generated IDs use usr_...
card.idstringyesV-REC card public IDcards.public_id; card_...
card.card_typestringyesCard typeVIRTUAL|PHYSICAL
card.brandstring|nullyesCard brandcards.brand nullable
card.masked_panstring|nullyesMasked PANnever full PAN; nullable
card.last4string|nullyesLast four PAN digitsnullable
card.currencystringyesCard currencycards.currency
card.statusstringyesCard statusACTIVE|FROZEN|CLOSED (stored/provider-normalized)
card.atm_enabledbooleanyesATM flag explicitly boolean-cast in mutation responsesexplicit (bool) cast by response cardShape(); key always present
card.created_atstringyesCard record creation timecards.created_at NOT NULL DB datetime; key always present

HTTP 201 · Expected response

{
    "user_id": "usr_demo",
    "card": {
        "id": "card_demo",
        "card_type": "PHYSICAL",
        "brand": "MASTERCARD",
        "masked_pan": "**** **** **** 1234",
        "last4": "1234",
        "currency": "USD",
        "status": "ACTIVE",
        "atm_enabled": true,
        "created_at": "2026-10-05 01:00:00"
    }
}

Expected errors / HTTP responses

HTTP 400invalid_jsonFormat: JSON

Malformed JSON

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 404not_foundFormat: JSON

{id} is not an active/non-INACTIVE member of this Merchant

HTTP 422idempotency_key_required / assoc_number_required / invalid_assoc_number / kyc_not_approved / physical_link_requires_sakura_driver / sakura_cards_scope_required / idempotency_key_too_long / active_merchant_membership_required / vrec_instance_required / sakura_user_not_linkedFormat: JSON

Input/provider validation

HTTP 503physical_card_link_unavailableFormat: JSON

Upstream card-link unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Contract content type is application/json. Current controller does not enforce the Content-Type header, but json_decodes the raw body; the HMAC body hash covers the exact raw bytes.
GET /api/merchant/v1/users/balances Bulk-read balances (wallet information) of this Merchant's users balances.read
Auth Merchant HMAC Scope balances.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Query parameters

FieldTypeRequiredConstraint / meaning
currencystringnoBalance currency3-letter code; default BASE_CURRENCY (USD); invalid => 422 invalid_currency
limitintegernoPage size1–200, default 100
cursorstringnonext_cursor from the previous responseinvalid => 422 invalid_cursor

OUT · Response contract

FieldTypePresentConstraint / meaning
merchant_idstringyesMerchant public ID
currencystringyesBalance currency
data[]array<object>yesPer-user balances in ascending user order
data[].user_idstringyesV-REC user public IDusr_...
data[].beneficiary_idstringyesBeneficiary ID
data[].membership_statusstringyesMembership statusACTIVE etc. (INACTIVE is excluded)
data[].balance_view_allowedbooleanyesWhether the balance may be viewed (can_view_balance). No amounts when false
data[].balance_scopestringnoTENANT_MERCHANT / SOURCE_MERCHANT
data[].raw_balance / display_balance / held_balance / expired_locked / availabledecimal stringnoSame values as GET /users/{id}/balance (TENANT_MERCHANT only)
data[].read_only / allocatable / spendable / withdrawablebooleannoSame as GET /users/{id}/balance
data[].reference_balance_viastringnoFor READONLY (reference balance from another Merchant) no amount is returned in bulk; fetch it individually from this path
next_cursorstring|nullyesCursor for the next page (null when has_more=false)
has_morebooleanyesWhether more items exist

HTTP 200 · Expected response

{
    "merchant_id": "mrc_demo",
    "currency": "USD",
    "data": [
        {
            "user_id": "usr_demo",
            "beneficiary_id": "VREC-ABC123",
            "membership_status": "ACTIVE",
            "currency": "USD",
            "balance_view_allowed": true,
            "balance_scope": "TENANT_MERCHANT",
            "source_merchant_id": "MRC_DEMO",
            "read_only": false,
            "allocatable": true,
            "spendable": true,
            "withdrawable": true,
            "raw_balance": "1500.00000000",
            "display_balance": "1500.00000000",
            "held_balance": "100.00000000",
            "expired_locked": "0.00000000",
            "available": "1400.00000000"
        },
        {
            "user_id": "usr_ref",
            "beneficiary_id": "VREC-DEF456",
            "membership_status": "ACTIVE",
            "currency": "USD",
            "balance_view_allowed": true,
            "balance_scope": "SOURCE_MERCHANT",
            "source_merchant_id": "MRC_OTHER",
            "read_only": true,
            "allocatable": false,
            "spendable": false,
            "withdrawable": false,
            "reference_balance_via": "/api/merchant/v1/users/usr_ref/balance"
        },
        {
            "user_id": "usr_hidden",
            "beneficiary_id": "VREC-GHI789",
            "membership_status": "ACTIVE",
            "currency": "USD",
            "balance_view_allowed": false
        }
    ],
    "next_cursor": "MTIz",
    "has_more": true
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

Merchant HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Credential lacks balances.read

HTTP 422invalid_currency / invalid_cursorFormat: JSON

Invalid query value

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Returns this Merchant's non-INACTIVE users in ascending user order, up to 200 per call. While has_more is true, pass next_cursor as cursor.
  • Amounts and flags in each row equal GET /users/{id}/balance (TENANT_MERCHANT).
  • READONLY users (reference balance from another Merchant) need one SAKURA lookup each, so no amount is returned in bulk; use reference_balance_via. No amounts are returned for users with can_view_balance=false.
GET /api/merchant/v1/users/{id}/balance Get user balance scoped to this Merchant balances.read
Auth Merchant HMAC Scope balances.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...

Query parameters

FieldTypeRequiredConstraint / meaning
currencystringnoBalance currencyomitted=>BASE_CURRENCY; if query key exists, trim+strtoupper only. An explicitly empty currency remains empty string; no ISO validation in current controller.

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC user public ID
merchant_idstringyesMerchant public ID
currencystringyesBalance currency
balance_scopestringyesBalance scopeTENANT_MERCHANT (this Merchant) / SOURCE_MERCHANT (READONLY reference)
source_merchant_idstring|nullyesSource SAKURA Merchant of the user
read_onlybooleanyesWhether the membership is READONLY
allocatablebooleanyesWhether tenant funds can be allocated
spendablebooleanyesWhether the balance can be spent
withdrawablebooleanyesWhether the balance can be withdrawn
raw_balancedecimal-stringyesRaw accounting balance; may be negative
display_balancedecimal-stringyesDisplay balance clamped at zero
held_balancedecimal-stringyesAuthorization hold amount
expired_lockeddecimal-stringyesExpired/locked balance
availabledecimal-stringyesAvailable balancemax(display-held-expired, 0)

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "merchant_id": "mrc_demo",
    "currency": "USD",
    "balance_scope": "TENANT_MERCHANT",
    "source_merchant_id": "MRC_DEMO",
    "read_only": false,
    "allocatable": true,
    "spendable": true,
    "withdrawable": true,
    "raw_balance": "1500.00000000",
    "display_balance": "1500.00000000",
    "held_balance": "100.00000000",
    "expired_locked": "0.00000000",
    "available": "1400.00000000"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 404not_foundFormat: JSON

User outside Merchant boundary

HTTP 403balance_view_not_allowedFormat: JSON

Membership has can_view_balance = false

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

GET /api/merchant/v1/users/{id}/transactions List Merchant balance ledger entries transactions.read
Auth Merchant HMAC Scope transactions.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...

Query parameters

FieldTypeRequiredConstraint / meaning
limitintegernoMaximum entriesPHP (int) cast then clamp 1..200; omitted=>50; abc/0/negative=>1; >200=>200

OUT · Response contract

FieldTypePresentConstraint / meaning
data[].public_idstringyesMerchant balance entry IDvbe_...
data[].amountdecimal-stringyesAmount
data[].currencystringyesCurrency
data[].entry_typestringyesEntry type
data[].reference_typestringyesReference typeDB column NOT NULL; key always present
data[].reference_idstringyesReference IDDB column NOT NULL; key always present
data[].metadata_jsonstring|nullyesMetadata JSON stringkey always present; DB value nullable JSON text
data[].created_atstringyesCreated timestamp

HTTP 200 · Expected response

{
    "data": [
        {
            "public_id": "vbe_demo",
            "amount": "100.00",
            "currency": "USD",
            "entry_type": "CREDIT",
            "reference_type": "PAYOUT",
            "reference_id": "po_demo",
            "metadata_json": "{}",
            "created_at": "2026-10-05 01:00:00"
        }
    ]
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

HTTP 404not_foundFormat: JSON

User outside Merchant boundary

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

GET /api/merchant/v1/payouts List payouts attributed to this Merchant payouts.read
Auth Merchant HMAC Scope payouts.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

OUT · Response contract

FieldTypePresentConstraint / meaning
data[].public_idstringyesPayout public IDpo_...
data[].beneficiary_idstringyesBeneficiary ID
data[].amountdecimal-stringyesPayout amount
data[].currencystringyesCurrency
data[].external_referencestring|nullyesExternal referenceDB column nullable; key always present
data[].purposestringyesPurpose
data[].statusstringyesPayout status
data[].created_atstringyesCreated timestamp
data[].credited_atstring|nullyesCredited timestampkey always present; null until credited
data[].user_idstringyesV-REC user public ID
data[].emailstringyesUser email

HTTP 200 · Expected response

{
    "data": [
        {
            "public_id": "po_demo",
            "beneficiary_id": "VREC-DEMO",
            "amount": "100.00",
            "currency": "USD",
            "external_reference": "INV-1001",
            "purpose": "corporate_payout",
            "status": "CREDITED",
            "created_at": "2026-10-05 01:00:00",
            "credited_at": "2026-10-05 01:01:00",
            "user_id": "usr_demo",
            "email": "[email protected]"
        }
    ]
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Current implementation returns up to 200 newest records.
GET /api/merchant/v1/report Get Merchant aggregate report reports.read
Auth Merchant HMAC Scope reports.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Query parameters

FieldTypeRequiredConstraint / meaning
currencystringnoReport currencyomitted=>BASE_CURRENCY; trim+strtoupper only; explicitly empty remains empty string; current code does NOT validate ISO length/value.

OUT · Response contract

FieldTypePresentConstraint / meaning
merchant_idstringyesMerchant public ID
currencystringyesReport currency
raw_balancedecimal-stringyesRaw balance total
display_balancedecimal-stringyesDisplay balance clamped at zero
active_usersintegeryesNumber of active memberships
payoutsintegeryesTotal payout count

HTTP 200 · Expected response

{
    "merchant_id": "mrc_demo",
    "currency": "USD",
    "raw_balance": "15000.00",
    "display_balance": "15000.00",
    "active_users": 125,
    "payouts": 310
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

HMAC/timestamp/nonce/credential/signature/replay/rate/IP authentication failure

HTTP 403merchant_scope_deniedFormat: JSON

Required scope is missing

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

GET /api/merchant/v1/users/{id}/wallet Read a user's wallet balances (read-only) wallets.read
Auth Merchant HMAC Scope wallets.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user idusr_...

Query parameters

FieldTypeRequiredConstraint / meaning
skip_balancesanynoSkip on-chain balance reads when present

OUT · Response contract

FieldTypePresentConstraint / meaning
user_id / merchant_idstringyesV-REC user / merchant id
read_onlybooleanyesAlways true; merchants cannot operate wallets via API
has_walletbooleanyesWhether the user has a wallet
walletobject|nullyesaddress, version, network, chain_id, created_at
balancesobject|nullyesSame shape as wallet_summary.balances
gaslessobjectyesallowed, reason, daily_limit, daily_remaining
transfer_blockedbooleanyesSuspended by operator

HTTP 200 · Expected response

{
    "user_id": "usr_...",
    "merchant_id": "mrc_demo",
    "read_only": true,
    "has_wallet": true,
    "wallet": {
        "address": "0x3f5c...",
        "version": 1,
        "network": "ethereum",
        "chain_id": 1,
        "created_at": "2026-10-16T01:00:00+00:00"
    },
    "balances": {
        "usdt": {
            "symbol": "USDT",
            "contract": "0xdac17f958d2ee523a2206206994597c13d831ec7",
            "decimals": 6,
            "balance": "120.500000",
            "pending_incoming": "0.000000",
            "available": "120.500000"
        },
        "eth": {
            "symbol": "ETH",
            "balance": "0.000000000000000000"
        },
        "complete": true,
        "fetched_at": "2026-10-16T01:02:03+00:00"
    },
    "gasless": {
        "allowed": true,
        "reason": "merchant_managed",
        "daily_limit": 5,
        "daily_remaining": 4
    },
    "transfer_blocked": false
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_signature_invalid / ...Format: JSON

HMAC authentication failure

HTTP 403merchant_scope_denied / balance_view_not_allowedFormat: JSON

Missing wallets.read / membership cannot view balances

HTTP 404not_found / wallet_user_not_foundFormat: JSON

Not in this merchant / not linked on SAKURA

HTTP 409wallet_not_available / wallet_reference_unavailableFormat: JSON

SAKURA crypto_wallet not granted / no source for READONLY membership

HTTP 503wallet_service_unavailableFormat: JSON

Temporary failure

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Merchants cannot create, rotate or transfer via the API. Seed phrases, keys and wallet passwords are never returned.
GET /api/merchant/v1/crypto-sales Crypto sales, fees and royalties crypto_sales.read
Auth Merchant HMAC Scope crypto_sales.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Query parameters

FieldTypeRequiredConstraint / meaning
from / todatenoCompletion date rangeYYYY-MM-DD
limitintegernoRows1..500

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "merchant_id": "mrc_demo",
    "totals": {
        "sales_count": 2,
        "gross_usd": "110.000000",
        "company_fee_usd": "1.100000",
        "merchant_royalty_usd": "0.550000"
    },
    "totals_basis": "returned_rows",
    "data": [
        {
            "contract_no": "VCS-20261016-AB12CD34EF",
            "contract_type": "SALE",
            "status": "COMPLETED",
            "product": "SGP_USD",
            "product_name": "SGP-USD",
            "product_quantity": "98.500000",
            "seller": "SAKURA_EXCHANGE",
            "payment": {
                "asset": "USDT",
                "symbol": "USDT",
                "amount": "100.000000",
                "unit_price_usd": "1.00000000",
                "pay_to_address": "0x...",
                "tx_hash": null
            },
            "pricing": {
                "gross_usd": "100.000000",
                "company_fee_rate": "0.01000000",
                "company_fee_usd": "1.000000",
                "merchant_royalty_rate": "0.00500000",
                "merchant_royalty_usd": "0.500000",
                "net_usd": "98.500000",
                "points_per_usd": null
            },
            "merchant_responsibility": false,
            "quote_expires_at": "2026-10-16T01:10:00+00:00",
            "terms": {
                "version": "2026-10-16",
                "seller_name": "SAKURA EXCHANGE GUAM INC.",
                "ja": [
                    "本契約は、購入者が暗号資産(USDT)を代金として、売主から「SGP-USD」を購入する売買契約です。通貨の両替ではありません。"
                ],
                "en": [
                    "This is a sale contract …"
                ]
            },
            "replayed": false,
            "user_id": "usr_demo"
        }
    ]
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / ...Format: JSON

HMAC failure

HTTP 403merchant_scope_deniedFormat: JSON

Missing crypto_sales.read

HTTP 409wallet_not_availableFormat: JSON

SAKURA crypto_wallet not granted

HTTP 503crypto_sales_unavailableFormat: JSON

Temporary failure

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Completed sales of this Merchant's users only. Read-only; merchants cannot purchase on behalf of users.
GET /api/merchant/v1/crypto-sales/funding-status AML and manual prefund review status per merchant sale (read-only) crypto_funding.read
Auth Merchant HMAC Scope crypto_funding.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Query parameters

FieldTypeRequiredConstraint / meaning
contract_nostringnoCrypto sale contract number^VCS-[A-Za-z0-9-]{1,36}$
user_idstringnoMerchant-owned V-REC public user IDusr_...; cannot combine with user_ref
user_refstringnoSAKURA user reference^USR_[0-9]{1,16}$; cannot combine with user_id
aml_statusenumnoAML review statusPENDING|APPROVED|REJECTED
prefund_statusenumnoPrefund review statusPENDING|VERIFIED|REJECTED
card_funding_allowedenumnoCard-funding-allowed flag0|1
cursorstringnoOpaque cursor for next pagemax 256; Merchant-bound, expires in 1h; do not use after_id
limitintegernoPage size1-100; default server limit

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "merchant_id": "mrc_demo",
    "data": [
        {
            "contract_no": "VCS-DEMO",
            "user_id": "usr_demo",
            "aml_status": "APPROVED",
            "prefund_status": "VERIFIED",
            "card_funding_allowed": true
        }
    ],
    "has_more": false,
    "next_cursor": null,
    "funding_check_type": "MIXED_BY_ROW",
    "note": "Prefund status is an operator review record, not proof of a MatchMove balance."
}

Expected errors / HTTP responses

HTTP 401merchant_auth_failedFormat: JSON

Merchant HMAC authentication failed

HTTP 403merchant_scope_denied / balance_view_not_allowedFormat: JSON

crypto_funding.read not granted, or balance visibility denied

HTTP 404not_foundFormat: JSON

Requested user_id not owned by merchant

HTTP 409funding_status_not_enabled / funding_status_unavailable / user_sakura_link_unavailableFormat: JSON

SAKURA Connected Apps scope or linking not available

HTTP 422invalid_cursor / ambiguous_user_filter / invalid_...Format: JSON

Invalid filter or cursor

HTTP 503funding_capabilities_unavailable / funding_status_unavailableFormat: JSON

Required membership schema or connected service unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Read-only status API; does not execute AML or prefund decisions. Operator decisions remain in SAKURA admin.
  • Returns only ACTIVE FULL merchant memberships with can_view_balance=1. Maps internal SAKURA user refs to V-REC public IDs; no review reasons or admin IDs.
  • card_funding_allowed is not a spendable-balance amount. Manual prefund attestation is not proof of MatchMove card funds.
GET /api/merchant/v1/users/{id}/kyc/schema Fetch current SAKURA KYCFORM kyc.preload
Auth Merchant HMAC Scope kyc.preload Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC public user IDusr_...

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "form": {
        "fields": []
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_signature_invalidFormat: JSON

HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Missing explicit scope

HTTP 503sakura_unavailableFormat: JSON

SAKURA upstream failure

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Use current SAKURA fields and document keys, never assume fixed names.
POST /api/merchant/v1/users/{id}/kyc/preload Preload KYC data and evidence kyc.preload
Auth Merchant HMAC Scope kyc.preload Content-Type application/json Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-KeystringyesKYC submission idempotency key8..190 chars; raw JSON same on retry

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC public user IDusr_...

Request body

FieldTypeRequiredConstraint / meaning
fieldsobjectyesSAKURA KYCFORM scalar field valuesmax 100
documents.<field>.data_base64stringyesUp to 6 base64 imagesJPEG/PNG, <=5MiB each, <=10MiB total
consent.confirmedbooleanyesExplicit user consenttrue
consent.obtained_atdate-timeyesConsent timestamppast 30d
consent.evidence_refstringyesEvidence reference8..190 chars
consent.terms_versionstringyesConsent terms version

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC user ID
draft_savedbooleanyesPreload complete; false blocks link
kyc_statusstringyesKYC state (not necessarily APPROVED)
next_actionstring|object|nullnoNext action

HTTP 201 · Expected response

{
    "user_id": "usr_demo",
    "draft_saved": true,
    "kyc_status": "PENDING",
    "next_action": {
        "type": "ready_for_ekyc"
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_signature_invalidFormat: JSON

HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Missing explicit scope

HTTP 503sakura_unavailableFormat: JSON

SAKURA upstream failure

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Ambiguous response requires reconciliation, not blind resend.
  • Read exact field names from kyc/schema.
POST /api/merchant/v1/users/{id}/kyc/onboarding-link Retrieve SAKURA eKYC link for verified cardholder delivery kyc.link.issue
Auth Merchant HMAC Scope kyc.link.issue Content-Type application/json Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC public user IDusr_...

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC user ID
ekyc_urlstring|nullconditionalPrivate eKYC short link for cardholder
kyc_statusstringyesKYC status
next_actionstring|object|nullnoNext action

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "ekyc_url": "https://sakura.example/kyc/EXAMPLE",
    "kyc_status": "PENDING",
    "next_action": "send_link_to_user"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_signature_invalidFormat: JSON

HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Missing explicit scope

HTTP 503sakura_unavailableFormat: JSON

SAKURA upstream failure

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 202 means URL pending; 409 means draft/review/reconciliation gate.
  • Deliver privately to verified user; never log URL.
GET /api/merchant/v1/users/{id}/cards/{cardId}/issuer-status Check live MatchMove card status cards.read
Auth Merchant HMAC Scope cards.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC user public IDusr_...
cardIdstringyesV-REC card public IDcard_...

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesUser public IDusr_...
card_idstringyesCard public IDcard_...
issuer_statusstringyesLive issuer statusACTIVE|FROZEN|CLOSED
sourcestringyesSourceMATCHMOVE_LIVE
observed_atISO 8601yesObserved timeUTC

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "card_id": "card_demo",
    "issuer_status": "FROZEN",
    "source": "MATCHMOVE_LIVE",
    "observed_at": "2026-10-23T00:00:00Z"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_ip_not_allowedFormat: JSON

Merchant authentication, signature, timestamp, nonce or IP validation failed

HTTP 403merchant_scope_deniedFormat: JSON

No permission

HTTP 404card_not_foundFormat: JSON

Card not found

HTTP 503issuer_card_status_unavailableFormat: JSON

Issuer status cannot be verified

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Queries MatchMove, not stored SAKURA or V-REC card status.
BASE URL

Tenant Funding Merchant API

https://api.v-rec.xyz
GET /api/merchant/v1/tenant-funding/balance Read SAKURA-authoritative Tenant balance and expiry policy tenant_funding.read
Auth Merchant HMAC Scope tenant_funding.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

OUT · Response contract

FieldTypePresentConstraint / meaning
merchant_idstringyesV-REC Merchant public ID bound to the authenticated credentialmrc_...
tenant_funding.tenant_refstringyesSAKURA Tenant Account referenceVTA_...
tenant_funding.currencystringyesTenant funding currencyUSD
tenant_funding.source_assetstringyesBacking asset on SAKURAcurrent implementation: SGP-USD
tenant_funding.commitmentdecimal stringyesTenant accounting commitmentSAKURA-authoritative
tenant_funding.reserveddecimal stringyesAccounting amount currently reservedSAKURA-authoritative
tenant_funding.availabledecimal stringyesAccounting amount currently available for new allocationSAKURA-authoritative; execution re-checks
tenant_funding.fundeddecimal stringyesCumulative funded principalSAKURA-authoritative
tenant_funding.returneddecimal stringyesCumulative returned principalSAKURA-authoritative
tenant_funding.feesdecimal stringyesCumulative Tenant Funding feesSAKURA-authoritative
tenant_funding.funding_fee_fixeddecimal stringyesFixed fee charged per allocation. An allocation needs amount + this fee to be within availableSAKURA-authoritative; 0 when no fee is configured
tenant_funding.expiry_policystringyesCurrent effective funding expiry policye.g. EXPIRABLE_180 / NO_EXPIRY
tenant_funding.expiry_daysinteger|nullyesExpiry days; may be null for no-expiry policytypically 180 when EXPIRABLE_180
tenant_funding.statusstringyesSAKURA Tenant statusexpected ACTIVE for new funding
tenant_funding.modestringyesSAKURA Tenant Funding modeENFORCED required for new funding
tenant_funding.reconciliation_statusstringyesSAKURA reconciliation stateSUSPENDED_RECONCILIATION blocks new movement
tenant_funding.last_reconciled_atstring|nullyesLast reconciliation timeUTC/ISO-style value from SAKURA

HTTP 200 · Expected response

{
    "merchant_id": "mrc_demo",
    "tenant_funding": {
        "tenant_ref": "VTA_12345678",
        "currency": "USD",
        "source_asset": "SGP-USD",
        "commitment": "15000.00000000",
        "reserved": "0.00000000",
        "available": "12497.50000000",
        "funded": "2500.00000000",
        "returned": "0.00000000",
        "fees": "2.50000000",
        "funding_fee_fixed": "2.50000000",
        "expiry_policy": "EXPIRABLE_180",
        "expiry_days": 180,
        "status": "ACTIVE",
        "mode": "ENFORCED",
        "reconciliation_status": "MATCHED",
        "last_reconciled_at": "2026-10-05T06:00:00Z"
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

Merchant HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Credential lacks the required tenant_funding scope

HTTP 422tenant_binding_not_foundFormat: JSON

No Tenant Funding binding exists for the authenticated Merchant

HTTP 503sakura_not_connected / tenant_not_found / tenant_funding_scope_not_granted / <upstream error>Format: JSON

SAKURA Connected App or authoritative Tenant Funding state is unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • V-REC does not maintain an independent authoritative Tenant available balance; the returned values come from SAKURA.
  • available is not a reservation. SAKURA/MatchMove re-validates capacity at allocation execution.
  • funding_fee_fixed is the fixed fee per allocation. An allocation requires amount + funding_fee_fixed to be within available.
  • Calling POST /tenant-funding/allocations above available moves no funds and returns HTTP 409 (status=REJECTED, error=insufficient_tenant_available).
GET /api/merchant/v1/tenant-funding/diagnostics Diagnose Tenant Funding operational readiness without moving funds tenant_funding.read
Auth Merchant HMAC Scope tenant_funding.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Query parameters

FieldTypeRequiredConstraint / meaning
user_idstringnoOptional V-REC user public ID for membership/allocation preflightmust belong to authenticated Merchant; omitted => Tenant-wide diagnostics only

OUT · Response contract

FieldTypePresentConstraint / meaning
merchant_idstringyesAuthenticated Merchant public IDmrc_...
diagnostics.schema.tenant_fundingbooleanyesTenant Funding schema is installedread-only check
diagnostics.schema.membership_capabilitiesbooleanyesFULL/READONLY capability schema is installedread-only check
diagnostics.schema.direct_allocationbooleanyesv2.4.2 direct-allocation schema is installedread-only check
diagnostics.bindingobject|nullyesTenant binding for the current Merchanttenant_ref/local_mode/tenant_lots_withdrawable
diagnostics.sakura.reachablebooleanyesWhether SAKURA authoritative Tenant Funding state was reachableno fund movement
diagnostics.sakura.statusobject|nullyesSAKURA Tenant management/status resultread-only
diagnostics.sakura.balanceobject|nullyesSAKURA-authoritative Tenant balanceread-only
diagnostics.operationsobjectyesCurrent readiness for balance/capacity/allocate/reverse/return operationsdiagnostic flags only
diagnostics.userobject|nullyesMembership/source-target Merchant/KYC/SAKURA-link/capacity information when user_id is suppliedonly a user inside the authenticated Merchant boundary is returned

HTTP 200 · Expected response

{
    "merchant_id": "mrc_demo",
    "diagnostics": {
        "schema": {
            "tenant_funding": true,
            "membership_capabilities": true,
            "direct_allocation": true
        },
        "binding": {
            "tenant_ref": "VTA_12345678",
            "local_mode": "ENFORCED",
            "tenant_lots_withdrawable": true
        },
        "sakura": {
            "reachable": true,
            "status": {
                "management_enabled": true,
                "api_execute_enabled": true
            },
            "balance": {
                "available": "12497.50",
                "expiry_policy": "EXPIRABLE_180"
            },
            "error": null
        },
        "user": {
            "public_id": "usr_demo",
            "found": true,
            "kyc_status": "APPROVED",
            "membership_status": "ACTIVE",
            "access_mode": "FULL",
            "source_sakura_merchant_ref": "MRC_A",
            "target_sakura_merchant_ref": "MRC_A",
            "sakura_user_link_present": true,
            "can_receive_tenant_funds": true,
            "capacity": {
                "allocatable_amount": "12497.50"
            }
        },
        "operations": {
            "balance_read": true,
            "capacity_check": true,
            "allocate": true,
            "allocation_status_read": true,
            "allocation_reverse": true,
            "user_return": false
        }
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_signature_invalid / ...Format: JSON

Merchant HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

tenant_funding.read scope is missing

HTTP 503tenant_funding_diagnostics_failedFormat: JSON

Diagnostics could not be produced due to a transient failure

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • This endpoint never calls fund/return/reverse. It is a read-only preflight before real money movement.
  • A READONLY user may still have masked-card visibility, while operations.allocate=false exposes the source/target Merchant boundary.
GET /api/merchant/v1/tenant-funding/users/{id}/capacity Check whether/how much Tenant funding can be allocated to a user (indicative maximum) tenant_funding.read
Auth Merchant HMAC Scope tenant_funding.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idpath stringyesV-REC user public IDusr_...; membership must belong to authenticated Merchant

Query parameters

FieldTypeRequiredConstraint / meaning
currencystringnoCapacity currencydefault USD; current implementation accepts USD only

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesTarget V-REC user public IDusr_...
merchant_idstringyesAuthenticated Merchant public IDmrc_...
eligiblebooleanyesWhether this membership is eligible to receive Tenant fundssuccessful response is true
currencystringyesCapacity currencyUSD
allocatable_amountdecimal stringyesIndicative maximum you can allocate now (tenant available − allocation fee, floored to cents)non-reserving preflight; execution re-checks SAKURA and the Merchant MatchMove wallet
tenant_availabledecimal stringyesTenant available amount (SAKURA-authoritative)same source as balance.available
funding_feedecimal stringyesFixed fee per allocation0 when no fee is configured
user_tenant_origin_remainingdecimal stringyesUser's remaining unspent principal originating from this TenantV-REC provenance ledger
tenant_refstringyesTenant referenceVTA_...
access_modestringyesMembership access modeFULL on successful capacity response
expiry_policystring|nullyesCurrent expiry policy expected for new allocationfinal snapshot is returned by SAKURA on execution
expiry_daysinteger|nullyesCurrent expiry dayscommonly 180 for EXPIRABLE_180
reconciliation_statusstring|nullyesTenant reconciliation statenew movement requires healthy state

HTTP 200 · Expected response

{
    "user_id": "usr_demo",
    "merchant_id": "mrc_demo",
    "eligible": true,
    "currency": "USD",
    "allocatable_amount": "12495.00",
    "tenant_available": "12497.50000000",
    "funding_fee": "2.50000000",
    "user_tenant_origin_remaining": "500.00000000",
    "tenant_ref": "VTA_12345678",
    "access_mode": "FULL",
    "expiry_policy": "EXPIRABLE_180",
    "expiry_days": 180,
    "reconciliation_status": "MATCHED"
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

Merchant HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Credential lacks the required tenant_funding scope

HTTP 404not_foundFormat: JSON

User is not a membership of this Merchant

HTTP 403tenant_funding_not_allowed_for_membership / cross_merchant_tenant_funding_not_allowedFormat: JSON

READONLY, can_receive_tenant_funds=false, or source Merchant differs from the Tenant Merchant

HTTP 422tenant_funding_usd_only / tenant_membership_not_activeFormat: JSON

Non-USD request or membership is not ACTIVE

HTTP 503membership_capabilities_not_installed / tenant_funding_not_enabled_in_sakura / <upstream error>Format: JSON

Required migration is missing, SAKURA Tenant Funding is disabled, or authoritative state is unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • READONLY may still read masked card metadata under cards.read, but this capacity endpoint returns 403 and cannot be used for fund allocation.
  • allocatable_amount = tenant_available − funding_fee, floored to cents. Use it to validate the amount before allocating (TopUp).
  • This is a non-reserving preflight. At execution SAKURA re-checks the tenant balance and the Merchant MatchMove wallet; if short, the allocation returns 409 / REJECTED.
  • Ineligible memberships (READONLY, can_receive_tenant_funds=false, other source Merchant) return 403. Users without approved KYC fail at allocation with 422 (kyc_not_approved).
POST /api/merchant/v1/tenant-funding/allocations Allocate Tenant funds directly to a V-REC user tenant_funding.write
Auth Merchant HMAC Scope tenant_funding.write Content-Type application/json Success HTTP 201 / 202

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-Keyheader stringyesIdempotency key for the logical allocationtrimmed non-empty; max 190 chars; reuse with different user/amount => 409

Request body

FieldTypeRequiredConstraint / meaning
user_idstringyesTarget V-REC user public IDusr_...; ACTIVE/FULL same-source Merchant membership required
amountdecimal stringyesAllocation principalpositive USD; exact cent precision

OUT · Response contract

FieldTypePresentConstraint / meaning
allocation.idstringyesV-REC Tenant Allocation operation IDvta_...
allocation.user_idstringyesTarget V-REC user public IDusr_...
allocation.amountdecimal stringyesRequested principalpositive USD amount with cent precision
allocation.currencystringyesCurrencyUSD
allocation.statusstringyesAllocation state. COMPLETED = credited; REJECTED = not executed, no funds moved (final); FAILED/MISMATCH = needs review (final); SUBMITTING/UNKNOWN/RECONCILE_REQUIRED = not final (retry with the same Idempotency-Key)SUBMITTING / UNKNOWN / RECONCILE_REQUIRED / COMPLETED / REJECTED / FAILED / MISMATCH
allocation.error_codestring|nullyesMachine-readable reason code (e.g. why REJECTED). Examples: insufficient_tenant_available, insufficient_merchant_physicalnull when not applicable
allocation.sakura_request_uuidstring|nullyesSAKURA funding request UUIDpresent after upstream acknowledgement
allocation.errorstring|nullyesDiagnostic code/summary for non-success statesdoes not expose secrets
allocation.created_atstring|nullyesOperation creation timeUTC database timestamp
allocation.completed_atstring|nullyesCompletion timenull until completed

HTTP 201 / 202 · Expected response

{
    "allocation": {
        "id": "vta_demo",
        "user_id": "usr_demo",
        "amount": "100.00000000",
        "currency": "USD",
        "status": "COMPLETED",
        "error_code": null,
        "sakura_request_uuid": "01J...",
        "error": null,
        "created_at": "2026-10-05 07:00:00",
        "completed_at": "2026-10-05 07:00:01"
    }
}

HTTP 409 · Example when not executed (REJECTED)

{
    "error": "insufficient_tenant_available",
    "allocation": {
        "id": "vta_demo2",
        "user_id": "usr_demo",
        "amount": "50000.00000000",
        "currency": "USD",
        "status": "REJECTED",
        "error_code": "insufficient_tenant_available",
        "sakura_request_uuid": null,
        "error": "insufficient_tenant_available: Tenant available is insufficient. available=12497.50, required=50002.50",
        "created_at": "2026-10-05 07:00:00",
        "completed_at": null
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

Merchant HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Credential lacks the required tenant_funding scope

HTTP 400invalid_jsonFormat: JSON

Request body is not a JSON object

HTTP 404not_foundFormat: JSON

Target user is not part of this Merchant

HTTP 422idempotency_key_required / user_id_and_amount_required / invalid_amount / kyc_not_approved / tenant_membership_not_active / sakura_user_link_missing / tenant_funding_not_installedFormat: JSON

Required fields, amount, KYC, membership, SAKURA link or migration prerequisite is not satisfied

HTTP 403tenant_funding_not_allowed_for_membership / cross_merchant_tenant_funding_not_allowedFormat: JSON

New fund movement to a READONLY or cross-Merchant membership

HTTP 409insufficient_tenant_available / insufficient_merchant_physical / tenant_funding_not_enabled / user_mm_missing / merchant_mm_missing / user_not_in_tenant_program / idempotency_key_reused / operation_in_progress / unresolved_request_exists / tenant_allocation_rejectedFormat: JSON

Rejected without moving funds (final). Typical reasons: amount + fee exceeds tenant available, Merchant MatchMove wallet short, Tenant Funding disabled, Idempotency-Key reused with different parameters, or another money operation for the same user in progress (operation_in_progress / unresolved_request_exists). REJECTED moved no money; fix the cause and retry with a new Idempotency-Key. The body contains error and allocation (status=REJECTED, error_code)

HTTP 503membership_capabilities_not_installed / tenant_allocation_api_not_installed / tenant_funding_not_enabled_in_sakura / tenant_funding_internal_error / <upstream error>Format: JSON

Tenant Funding disabled, ambiguous SAKURA/MatchMove outcome, or transient internal failure

HTTP 202(allocation.status = SUBMITTING / UNKNOWN / RECONCILE_REQUIRED)Format: JSON

Outcome not final (temporary conflict, network). Funds may have moved: do not create a new operation; resend with the same Idempotency-Key or confirm via the list/detail endpoints

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 201 = COMPLETED (credited). 409 = REJECTED (not executed, no funds moved, final). 202 = not final (resend with the same Idempotency-Key).
  • Resending a REJECTED operation with the same Idempotency-Key returns the same result. Fix the condition (amount/balance) and use a new Idempotency-Key.
  • The V-REC provenance lot is created only after SAKURA confirms COMPLETED for the exact requested principal.
  • V-REC snapshots SAKURA expiry_policy/is_expirable/granted_at/expires_at on the lot; an applicable 180-day limit is preserved.
GET /api/merchant/v1/tenant-funding/allocations List Tenant Allocation (TopUp) results updated since a given time tenant_funding.read
Auth Merchant HMAC Scope tenant_funding.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Query parameters

FieldTypeRequiredConstraint / meaning
updated_sinceISO-8601 stringnoReturn only allocations updated at or after this timee.g. 2026-10-15T00:00:00+09:00; without offset = UTC; invalid => 422 invalid_updated_since
statusstringnoFilter by status (comma-separated)SUBMITTING / UNKNOWN / RECONCILE_REQUIRED / COMPLETED / REJECTED / FAILED / MISMATCH; other => 422 invalid_status
user_idstringnoFilter by userusr_...; not in this Merchant => 404
limitintegernoPage size1–200, default 100
cursorstringnonext_cursor from the previous responseinvalid => 422 invalid_cursor

OUT · Response contract

FieldTypePresentConstraint / meaning
data[]array<object>yesAllocations; each item has the allocation fields plus updated_at
data[].idstringyesAllocation IDvta_...
data[].user_idstringyesTarget V-REC user public IDusr_...
data[].amountdecimal stringyesRequested principal
data[].currencystringyesCurrencyUSD
data[].statusstringyesAllocation statesee allocation.status
data[].error_codestring|nullyesReason code
data[].sakura_request_uuidstring|nullyesSAKURA funding request UUID
data[].errorstring|nullyesDiagnostic message
data[].created_atstringyesCreation timeUTC 'YYYY-MM-DD HH:MM:SS'
data[].completed_atstring|nullyesCompletion time
data[].updated_atstringyesLast update time; changes whenever the result changesUTC 'YYYY-MM-DD HH:MM:SS'
next_cursorstring|nullyesCursor for the next pagepass as cursor; null when there is no item
has_morebooleanyesWhether more items exist

HTTP 200 · Expected response

{
    "data": [
        {
            "id": "vta_demo2",
            "user_id": "usr_demo",
            "amount": "50000.00000000",
            "currency": "USD",
            "status": "REJECTED",
            "error_code": "insufficient_tenant_available",
            "sakura_request_uuid": null,
            "error": "insufficient_tenant_available: Tenant available is insufficient. available=12497.50, required=50002.50",
            "created_at": "2026-10-15 01:00:00",
            "completed_at": null,
            "updated_at": "2026-10-15 01:00:01"
        },
        {
            "id": "vta_demo",
            "user_id": "usr_demo",
            "amount": "100.00000000",
            "currency": "USD",
            "status": "COMPLETED",
            "error_code": null,
            "sakura_request_uuid": "TF_01J...",
            "error": null,
            "created_at": "2026-10-15 01:05:00",
            "completed_at": "2026-10-15 01:05:01",
            "updated_at": "2026-10-15 01:05:01"
        }
    ],
    "next_cursor": "MjAyNi0xMC0xNSAwMTowNTowMXw0Mg",
    "has_more": false
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

Merchant HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Credential lacks tenant_funding.read

HTTP 404not_foundFormat: JSON

user_id is not a member of this Merchant

HTTP 422invalid_updated_since / invalid_status / invalid_cursorFormat: JSON

Invalid query value

HTTP 503tenant_allocation_api_not_installedFormat: JSON

v2.4.2 allocation migration has not been applied

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Sorted by updated_at ascending. While has_more is true, pass next_cursor as cursor to get the rest.
  • For cron, pass the last updated_at you processed as updated_since. An allocation whose result changes gets a newer updated_at and may be returned again; de-duplicate by id.
  • COMPLETED / REJECTED / FAILED / MISMATCH are final. SUBMITTING / UNKNOWN / RECONCILE_REQUIRED are not final (resend with the same Idempotency-Key).
  • A tenant.funding.allocation.updated webhook is also sent whenever the result changes.
GET /api/merchant/v1/tenant-funding/allocations/{id} Get a Tenant Allocation operation tenant_funding.read
Auth Merchant HMAC Scope tenant_funding.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idpath stringyesAllocation operation IDvta_...; must belong to authenticated Merchant

OUT · Response contract

FieldTypePresentConstraint / meaning
allocation.idstringyesV-REC Tenant Allocation operation IDvta_...
allocation.user_idstringyesTarget V-REC user public IDusr_...
allocation.amountdecimal stringyesRequested principalpositive USD amount with cent precision
allocation.currencystringyesCurrencyUSD
allocation.statusstringyesAllocation state. COMPLETED = credited; REJECTED = not executed, no funds moved (final); FAILED/MISMATCH = needs review (final); SUBMITTING/UNKNOWN/RECONCILE_REQUIRED = not final (retry with the same Idempotency-Key)SUBMITTING / UNKNOWN / RECONCILE_REQUIRED / COMPLETED / REJECTED / FAILED / MISMATCH
allocation.error_codestring|nullyesMachine-readable reason code (e.g. why REJECTED). Examples: insufficient_tenant_available, insufficient_merchant_physicalnull when not applicable
allocation.sakura_request_uuidstring|nullyesSAKURA funding request UUIDpresent after upstream acknowledgement
allocation.errorstring|nullyesDiagnostic code/summary for non-success statesdoes not expose secrets
allocation.created_atstring|nullyesOperation creation timeUTC database timestamp
allocation.completed_atstring|nullyesCompletion timenull until completed

HTTP 200 · Expected response

{
    "allocation": {
        "id": "vta_demo",
        "user_id": "usr_demo",
        "amount": "100.00000000",
        "currency": "USD",
        "status": "COMPLETED",
        "error_code": null,
        "sakura_request_uuid": "01J...",
        "error": null,
        "created_at": "2026-10-05 07:00:00",
        "completed_at": "2026-10-05 07:00:01"
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

Merchant HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Credential lacks the required tenant_funding scope

HTTP 404tenant_allocation_not_foundFormat: JSON

Allocation does not exist or belongs to another Merchant

HTTP 503tenant_allocation_api_not_installedFormat: JSON

v2.4.2 allocation migration has not been applied

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

POST /api/merchant/v1/tenant-funding/allocations/{id}/reverse Return only unspent principal originating from one specific allocation tenant_funding.write
Auth Merchant HMAC Scope tenant_funding.write Content-Type application/json Success HTTP 200 / 202

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-Keyheader stringyesIdempotency key for this reversaltrimmed non-empty; max 190 chars

Path parameters

FieldTypeRequiredConstraint / meaning
idpath stringyesAllocation operation ID to reversevta_...; must be COMPLETED

Request body

FieldTypeRequiredConstraint / meaning
amountdecimal stringyesAmount to returnpositive USD; exact cent precision; cannot exceed this allocation's remaining lot principal

OUT · Response contract

FieldTypePresentConstraint / meaning
return.idstringyesV-REC Tenant Return operation IDvtr_...
return.amountdecimal stringyesPrincipal returned to the Tenantpositive USD amount with cent precision
return.currencystringyesCurrencyUSD
return.statusstringyesReturn stateRESERVED / SUBMITTED / RECONCILE_REQUIRED / FAILED / COMPLETED
return.sakura_request_uuidstring|nullyesSAKURA return/funding request UUIDmay be null before acknowledgement
return.errorstring|nullyesDiagnostic code/summary for non-success states
return.created_atstring|nullyesOperation creation timeUTC database timestamp
return.updated_atstring|nullyesLast update timeUTC database timestamp

HTTP 200 / 202 · Expected response

{
    "return": {
        "id": "vtr_demo",
        "amount": "25.00000000",
        "currency": "USD",
        "status": "COMPLETED",
        "sakura_request_uuid": "01J...",
        "error": null,
        "created_at": "2026-10-05 07:10:00",
        "updated_at": "2026-10-05 07:10:01"
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

Merchant HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Credential lacks the required tenant_funding scope

HTTP 400invalid_jsonFormat: JSON

Request body is not a JSON object

HTTP 404tenant_allocation_not_foundFormat: JSON

Allocation does not exist or belongs to another Merchant

HTTP 422idempotency_key_required / amount_required / invalid_amount / tenant_allocation_not_reversibleFormat: JSON

Missing input, invalid amount, or allocation is not COMPLETED/reversible

HTTP 409reverse_amount_exceeds_remaining / tenant_return_in_progress / idempotency_key_reused / insufficient_tenant_origin_fundsFormat: JSON

Requested reverse exceeds this allocation's remaining principal, another return is in progress, or idempotency conflicts

HTTP 503<upstream error>Format: JSON

SAKURA outcome cannot be determined temporarily

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • This endpoint consumes only lots carrying the specified allocation's sakura_request_uuid and never consumes another allocation.
  • Already-consumed principal cannot be reversed.
POST /api/merchant/v1/tenant-funding/users/{id}/return Return this Tenant's remaining user-held principal to the Tenant tenant_funding.write
Auth Merchant HMAC Scope tenant_funding.write Content-Type application/json Success HTTP 200 / 202

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-Keyheader stringyesIdempotency key for the logical returntrimmed non-empty; max 190 chars

Path parameters

FieldTypeRequiredConstraint / meaning
idpath stringyesTarget V-REC user public IDusr_...; membership must belong to authenticated Merchant

Request body

FieldTypeRequiredConstraint / meaning
amountdecimal stringyesReturn amountpositive USD; exact cent precision; limited to this Tenant's remaining origin lots

OUT · Response contract

FieldTypePresentConstraint / meaning
return.idstringyesV-REC Tenant Return operation IDvtr_...
return.amountdecimal stringyesPrincipal returned to the Tenantpositive USD amount with cent precision
return.currencystringyesCurrencyUSD
return.statusstringyesReturn stateRESERVED / SUBMITTED / RECONCILE_REQUIRED / FAILED / COMPLETED
return.sakura_request_uuidstring|nullyesSAKURA return/funding request UUIDmay be null before acknowledgement
return.errorstring|nullyesDiagnostic code/summary for non-success states
return.created_atstring|nullyesOperation creation timeUTC database timestamp
return.updated_atstring|nullyesLast update timeUTC database timestamp

HTTP 200 / 202 · Expected response

{
    "return": {
        "id": "vtr_demo",
        "amount": "25.00000000",
        "currency": "USD",
        "status": "COMPLETED",
        "sakura_request_uuid": "01J...",
        "error": null,
        "created_at": "2026-10-05 07:10:00",
        "updated_at": "2026-10-05 07:10:01"
    }
}

Expected errors / HTTP responses

HTTP 401merchant_auth_required / merchant_timestamp_invalid / merchant_nonce_invalid / merchant_credential_invalid / merchant_signature_invalid / merchant_replay / merchant_rate_limited / merchant_ip_not_allowedFormat: JSON

Merchant HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

Credential lacks the required tenant_funding scope

HTTP 400invalid_jsonFormat: JSON

Request body is not a JSON object

HTTP 404not_foundFormat: JSON

Target user is not part of this Merchant

HTTP 422idempotency_key_required / amount_required / invalid_amount / sakura_user_link_missingFormat: JSON

Required input, amount, binding or SAKURA link prerequisite is not satisfied

HTTP 409insufficient_tenant_origin_funds / tenant_return_in_progress / idempotency_key_reusedFormat: JSON

Insufficient remaining principal originating from this Tenant, another return is in progress, or idempotency conflict

HTTP 503tenant_funding_not_enforced / <upstream error>Format: JSON

SAKURA outcome cannot be determined temporarily

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Only lots matching the authenticated Merchant, exact Tenant ref and VREC_TENANT provenance are returnable. Cross-Merchant reference balances are not eligible.
  • This is a cleanup/return path for funds already attributed to this Tenant; it does not authorize new cross-Merchant allocation.
BASE URL

Authenticated User Session API

https://v-rec.xyz
GET /api/v1/me Get authenticated user profile summary
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

FieldTypePresentConstraint / meaning
idstringyesV-REC user public IDusr_...
beneficiary_idstringyesBeneficiary ID
emailstringyesCurrent SAKURA email with local fallbackemail
namestringyesCurrent SAKURA name; empty string when absent
countrystring|nullyesSAKURA profile.country_codekey always present; nullable
kyc_statusstringyesKYC status from SAKURA with local fallback

HTTP 200 · Expected response

{
    "id": "usr_demo",
    "beneficiary_id": "VREC-DEMO",
    "email": "[email protected]",
    "name": "Taro Yamada",
    "country": "JP",
    "kyc_status": "APPROVED"
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 503sakura_profile_unavailableFormat: JSON

SAKURA profile unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

GET /api/v1/me/kyc Synchronize authenticated user KYC status
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

FieldTypePresentConstraint / meaning
statusstringyesNormalized KYC statusNOT_STARTED|PENDING|APPROVED|REJECTED|REVIEW|CORRECTION_REQUIRED
checked_atstringyesLast sync timestamp updated by this callDB UTC datetime

HTTP 200 · Expected response

{
    "status": "APPROVED",
    "checked_at": "2026-10-05 01:00:00"
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 409kyc_service_unavailableFormat: JSON

Sanitized upstream KYC conflict

HTTP 422kyc_service_unavailableFormat: JSON

Sanitized upstream KYC validation state

HTTP 429kyc_service_unavailableFormat: JSON

Upstream KYC rate limit

HTTP 503kyc_service_unavailableFormat: JSON

KYC service unavailable; correlation_id is returned

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

GET /api/v1/me/kyc/form Fetch the live SAKURA KYCFORM definition
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

FieldTypePresentConstraint / meaning
user_refstringconditionalSAKURA user ref added by Connected Apps bridgepresent in current connected_apps path; mock test driver omits
kyc_statusstringyesSAKURA KYC statuscurrent SAKURA adapter: NOT_STARTED|PENDING|APPROVED|CORRECTION_REQUIRED
editablebooleanyesWhether form is editable
fields[]array<object>yesSAKURA KYCFORM plus profile prerequisites not already defined in KYCFORM
fields[].namestringyesField name to submit
fields[].labelstringyesDisplay label
fields[].typeintegeryesSAKURA field type2=file, 3=textarea handling, 4=date handling; other values treated as string/select as configured
fields[].requiredbooleanyesRequired flag
fields[].controlstringyesRecommended UI controlfile|select|textarea|date|text
fields[].valuemixed|nullyesCurrent value; file fields always nullkey always present
fields[].has_filebooleanyesWhether an existing file is stored
fields[].optionsarray<object>yesCandidate list; may be empty for non-select fields
fields[].options[].codestringyesCandidate code to submit
fields[].options[].labelstringyesCandidate label
fields[].sectionstringyesDefinition sectionkyc_form|profile
ekycobjectconditionaleKYC public state in Connected Apps pathmock test driver omits
ekyc.statestringyesSAKURA eKYC stateverified|link_ready|link_refresh_required|provider_processing|link_missing_recovery|issue_failed_editable|rejected|issue_required|not_started
ekyc.verifiedbooleanyesVerified flag
ekyc.has_historybooleanyesWhether eKYC history exists
ekyc.has_linkbooleanyesWhether an eKYC link exists
ekyc.has_active_linkbooleanyesBackward-compatible alias of has_link
ekyc.provider_session_freshbooleanyesWhether provider session is fresh
ekyc.action_urlstring|nullyesSAKURA stable short URL; end-user session onlysensitive bearer-style URL; nullable
ekyc.expires_atstring|nullyeseKYC link expiryISO 8601 or null
ekyc.editablebooleanyesWhether KYC is currently editable
ekyc.can_issuebooleanyesWhether eKYC can be issued/reissued
ekyc.can_openbooleanyesWhether current link can be opened
next_action.typestringyesNext client actionredirect|completed|wait|correct|retry
next_action.urlstringconditionalSAKURA short URL only when type=redirectomitted otherwise
next_action.retry_after_secondsintegerconditionalOnly when type=waitcurrently 5

HTTP 200 · Expected response

{
    "user_ref": "USR_00000001",
    "kyc_status": "NOT_STARTED",
    "editable": true,
    "fields": [
        {
            "name": "mobile_country_code",
            "label": "Country code",
            "type": 1,
            "required": true,
            "control": "select",
            "value": "+81",
            "has_file": false,
            "options": [
                {
                    "code": "+81",
                    "label": "JPN (+81)"
                }
            ],
            "section": "kyc_form"
        }
    ],
    "ekyc": {
        "state": "not_started",
        "verified": false,
        "has_history": false,
        "has_link": false,
        "has_active_link": false,
        "provider_session_fresh": false,
        "action_url": null,
        "expires_at": null,
        "editable": true,
        "can_issue": false,
        "can_open": false
    },
    "next_action": {
        "type": "correct"
    }
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 409kyc_service_unavailableFormat: JSON

Upstream conflict

HTTP 422kyc_service_unavailableFormat: JSON

Upstream validation state

HTTP 429kyc_service_unavailableFormat: JSON

Upstream rate limit

HTTP 503kyc_service_unavailableFormat: JSON

Temporary service failure

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • SAKURA KYCFORM is authoritative. Profile prerequisites absent from KYCFORM (defaults: birthday/title/gender) are appended with section=profile.
  • Each fields[] object has name,label,type,required,control,value,has_file,options,section. Current code does not return source, prefill, or prefill_source.
  • next_action.url / ekyc.action_url are sensitive bearer-style short URLs. Do not forward or persist them in Merchant backends.
POST /api/v1/me/kyc/submit Submit the dynamic KYCFORM in the authenticated user session
Auth V-REC End-user Session Content-Type multipart/form-data (required for file fields) OR application/x-www-form-urlencoded (text-only submission) Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionmust be a form/multipart field in $_POST; JSON body does not satisfy Security::requireCsrf()
<fields[].name>string|filedynamicOnly names returned by GET /api/v1/me/kyc/form are acceptedunknown fields => 422 validation_error
<select field>stringdynamicFor control=select submit options[].code onlyout-of-catalog => 422 validation_error; strict catalogs fail closed if unavailable
<file field>filedynamicType=2 file fieldimage; png/jpg/jpeg; max 5120 KB; required only if required=true and no existing file
<textarea field>stringdynamicType=3 textmax 5000 when no option catalog
<date field>stringdynamicType=4 dateKYCFORM: valid date; profile prerequisite: Y-m-d; birthday before today
<other string field>stringdynamicOther string fieldsmax 255 when no option catalog

OUT · Response contract

FieldTypePresentConstraint / meaning
user_refstringconditionalSAKURA user ref added in Connected Apps pathmock test driver omits
successbooleanyeseKYC issue result
kyc_statusstringyesKYC status after submit
editablebooleanyesEditability after submit
errorstringconditionalOnly when success=falseekyc_issue_failed
reason_codestringconditionalAllow-listed reason code when success=falseidentity_mobile_already_registered|identity_email_already_registered|identity_already_registered|provider_session_unavailable
reason_fieldstring|nullconditionalRelated field on failure; nullable
ekycobjectconditionaleKYC public state in Connected Apps pathmock test driver omits
ekyc.statestringyesSAKURA eKYC stateverified|link_ready|link_refresh_required|provider_processing|link_missing_recovery|issue_failed_editable|rejected|issue_required|not_started
ekyc.verifiedbooleanyesVerified flag
ekyc.has_historybooleanyesWhether eKYC history exists
ekyc.has_linkbooleanyesWhether an eKYC link exists
ekyc.has_active_linkbooleanyesBackward-compatible alias of has_link
ekyc.provider_session_freshbooleanyesWhether provider session is fresh
ekyc.action_urlstring|nullyesSAKURA stable short URL; end-user session onlysensitive bearer-style URL; nullable
ekyc.expires_atstring|nullyeseKYC link expiryISO 8601 or null
ekyc.editablebooleanyesWhether KYC is currently editable
ekyc.can_issuebooleanyesWhether eKYC can be issued/reissued
ekyc.can_openbooleanyesWhether current link can be opened
next_action.typestringyesNext client actionredirect|completed|wait|correct|retry
next_action.urlstringconditionalSAKURA short URL only when type=redirectomitted otherwise
next_action.retry_after_secondsintegerconditionalOnly when type=waitcurrently 5

HTTP 200 · Expected response

{
    "user_ref": "USR_00000001",
    "success": true,
    "kyc_status": "PENDING",
    "editable": false,
    "ekyc": {
        "state": "link_ready",
        "verified": false,
        "has_history": true,
        "has_link": true,
        "has_active_link": true,
        "provider_session_fresh": true,
        "action_url": "https://sakura.example/ekyc/short-token",
        "expires_at": "2026-10-05T01:15:00+00:00",
        "editable": false,
        "can_issue": false,
        "can_open": true
    },
    "next_action": {
        "type": "redirect",
        "url": "https://sakura.example/ekyc/short-token"
    }
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Missing/mismatched _csrf. Security::requireCsrf() runs before Auth::requireUser().

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 422validation_errorFormat: JSON

Per-field validation errors with errors map and correlation_id

HTTP 409kyc_service_unavailableFormat: JSON

Conflict such as concurrent processing

HTTP 429kyc_service_unavailableFormat: JSON

Upstream rate limit

HTTP 503kyc_service_unavailableFormat: JSON

Temporary KYC service failure

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • HTTP 200 can still carry success=false. In that case inspect error=ekyc_issue_failed and reason_code/reason_field.
  • Concurrent KYC submissions for the same user are serialized by a SAKURA lock; a second concurrent request becomes 422 validation_error.
  • Current code reads PHP $_POST (and $_FILES for KYC), not a JSON request body.
GET /api/v1/me/balance Get user V-REC and per-Merchant balances
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

FieldTypePresentConstraint / meaning
scope_instance_idintegeryesCurrent V-REC instance internal IDauthenticated Auth::user requires a positive active instance id
vrec_rawdecimal-stringyesRaw V-REC balance
vrec_availabledecimal-stringyesSpendable balance
vrec_withdrawabledecimal-stringyesWithdrawable balance
authorization_holdsdecimal-stringyesAuthorization holds
expired_lockeddecimal-stringyesExpired locked balance when instance existspresent on authenticated path
merchant_balances[]array<object>yesPer-merchant balances
merchant_balances[].merchant_idintegeryesInternal merchant ID
merchant_balances[].merchant_public_idstringyesMerchant public ID
merchant_balances[].display_namestringyesMerchant display name
merchant_balances[].raw_balancedecimal-stringyesRaw balance
merchant_balances[].display_balancedecimal-stringyesDisplay balance
merchant_balances[].expired_lockeddecimal-stringconditionalExpired locked amount
merchant_balances[].availabledecimal-stringyesAvailable; zero when membership/merchant is inactive
merchant_balances[].membership_statusstringyesMembership status
merchant_balances[].merchant_statusstringyesMerchant status

HTTP 200 · Expected response

{
    "scope_instance_id": 1,
    "vrec_raw": "1500.00",
    "vrec_available": "1400.00",
    "vrec_withdrawable": "1300.00",
    "authorization_holds": "100.00",
    "expired_locked": "0.00",
    "merchant_balances": [
        {
            "merchant_id": 10,
            "merchant_public_id": "mrc_demo",
            "display_name": "Example Merchant",
            "raw_balance": "1500.00",
            "membership_status": "ACTIVE",
            "merchant_status": "ACTIVE",
            "display_balance": "1500.00",
            "expired_locked": "0.00",
            "available": "1500.00"
        }
    ]
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • No currency query parameter is read; the endpoint uses Config BASE_CURRENCY.
GET /api/v1/me/payouts List authenticated user payouts
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

FieldTypePresentConstraint / meaning
data[].idstringyesPayout ID
data[].amountdecimal-stringyesAmount
data[].currencystringyesCurrency
data[].external_referencestring|nullyesExternal referenceDB column nullable; key always present
data[].purposestringyesPurpose
data[].statusstringyesStatus
data[].created_atstringyesCreated timestamp
data[].credited_atstring|nullyesCredited timestampkey always present; null until credited
data[].companystringyesPartner/company display name

HTTP 200 · Expected response

{
    "data": [
        {
            "id": "po_demo",
            "amount": "100.00",
            "currency": "USD",
            "external_reference": "INV-1001",
            "purpose": "corporate_payout",
            "status": "CREDITED",
            "created_at": "2026-10-05 01:00:00",
            "credited_at": "2026-10-05 01:01:00",
            "company": "Example Corp"
        }
    ]
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Current implementation returns up to 100 newest records.
  • Direct tenant allocations are excluded. Use /api/v1/me/tenant-allocations.
GET /api/v1/me/transactions List authenticated user card transactions
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

FieldTypePresentConstraint / meaning
data[].idstringyesCard transaction ID
data[].transaction_typestringyesTransaction type
data[].amountdecimal-stringyesAmount
data[].currencystringyesCurrency
data[].mccstring|nullyesMerchant Category Codekey always present; value may be null
data[].statusstringyesStatus
data[].occurred_atstringyesOccurred at
data[].card_idstringyesV-REC card ID
data[].masked_panstring|nullyesMasked PANkey always present; value may be null

HTTP 200 · Expected response

{
    "data": [
        {
            "id": "txn_demo",
            "transaction_type": "PURCHASE",
            "amount": "25.00",
            "currency": "USD",
            "mcc": "5812",
            "status": "APPROVED",
            "occurred_at": "2026-10-05 00:30:00",
            "card_id": "card_demo",
            "masked_pan": "**** **** **** 1234"
        }
    ]
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Current implementation returns up to 100 newest records.
  • V-REC internal card_transactions only. Use /api/v1/me/cards/{id}/issuer-transactions for actual issuer statements.
GET /api/v1/me/cards List authenticated user cards
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

FieldTypePresentConstraint / meaning
data[]array<card_list_item>yesUser masked card list
data[].idstringyesV-REC card public IDcards.public_id; card_...
data[].card_typestringyesCard typeVIRTUAL|PHYSICAL
data[].brandstring|nullyesCard brandcards.brand nullable
data[].masked_panstring|nullyesMasked PANnever full PAN; nullable
data[].last4string|nullyesLast four PAN digitsnullable
data[].currencystringyesCard currencycards.currency
data[].statusstringyesCard statusACTIVE|FROZEN|CLOSED (stored/provider-normalized)
data[].atm_enabledintegeryesRaw ATM flag from DB; card-list responses return JSON number 0 or 1raw cards.atm_enabled from PDO SELECT; JSON number 0 or 1 (ATTR_STRINGIFY_FETCHES=false); NOT boolean-cast
data[].created_atstringyesCard record creation timecards.created_at NOT NULL DB datetime; key always present

HTTP 200 · Expected response

{
    "data": [
        {
            "id": "card_demo",
            "card_type": "VIRTUAL",
            "brand": "MASTERCARD",
            "masked_pan": "**** **** **** 1234",
            "last4": "1234",
            "currency": "USD",
            "status": "ACTIVE",
            "atm_enabled": false,
            "created_at": "2026-10-01 00:00:00"
        }
    ]
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

POST /api/v1/me/cards/{type} Issue a virtual card; physical issuance retired
Auth V-REC End-user Session Content-Type application/x-www-form-urlencoded OR multipart/form-data Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Path parameters

FieldTypeRequiredConstraint / meaning
typestringyesIssue typeVIRTUAL allowed; PHYSICAL returns 410 physical_card_issuance_retired (no issuer call)

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionmust be a form/multipart field in $_POST; JSON body does not satisfy Security::requireCsrf()

OUT · Response contract

FieldTypePresentConstraint / meaning
idstringyesV-REC card public IDcards.public_id; card_...
card_typestringyesCard typeVIRTUAL|PHYSICAL
brandstring|nullyesCard brandcards.brand nullable
masked_panstring|nullyesMasked PANnever full PAN; nullable
last4string|nullyesLast four PAN digitsnullable
currencystringyesCard currencycards.currency
statusstringyesCard statusACTIVE|FROZEN|CLOSED (stored/provider-normalized)
atm_enabledbooleanyesATM flag explicitly boolean-cast in mutation responsesexplicit (bool) cast by response cardShape(); key always present

HTTP 201 · Expected response

{
    "id": "card_demo",
    "card_type": "VIRTUAL",
    "brand": "MASTERCARD",
    "masked_pan": "**** **** **** 1234",
    "last4": "1234",
    "currency": "USD",
    "status": "ACTIVE",
    "atm_enabled": false
}

Expected errors / HTTP responses

HTTP 410physical_card_issuance_retiredFormat: JSON

Physical issuance is retired; use physical/link for existing cards.

HTTP 419CSRF validation failedFormat: non-JSON

Missing/mismatched _csrf. Security::requireCsrf() runs before Auth::requireUser().

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 422invalid_card_type / kyc_not_approved / active_merchant_membership_required / sakura_cards_scope_required / vrec_instance_required / sakura_user_not_linkedFormat: JSON

KYC/address/card issuance validation failed

HTTP 503card_issue_unavailableFormat: JSON

Card provider temporarily unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Current code reads PHP $_POST (and $_FILES for KYC), not a JSON request body.
POST /api/v1/me/cards/physical/link Link a pre-issued physical card with end-user step-up
Auth V-REC End-user Session Content-Type application/x-www-form-urlencoded OR multipart/form-data Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-KeystringrecommendedProvider idempotency keyoptional; trim; blank/omitted => normalized association number is used; non-empty >190 => idempotency_key_too_long

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionmust be a form/multipart field in $_POST; JSON body does not satisfy Security::requireCsrf()
current_passwordstringyesCurrent V-REC passwordstep-up verification
assoc_numberstringyesProxy / Association IDtrim -> remove ALL whitespace -> uppercase; then ^(?:PY)?\d{6,32}$; provider value normalized to PY + digits. Therefore raw form input containing spaces may still be accepted.

OUT · Response contract

FieldTypePresentConstraint / meaning
idstringyesV-REC card public IDcards.public_id; card_...
card_typestringyesCard typeVIRTUAL|PHYSICAL
brandstring|nullyesCard brandcards.brand nullable
masked_panstring|nullyesMasked PANnever full PAN; nullable
last4string|nullyesLast four PAN digitsnullable
currencystringyesCard currencycards.currency
statusstringyesCard statusACTIVE|FROZEN|CLOSED (stored/provider-normalized)
atm_enabledbooleanyesATM flag explicitly boolean-cast in mutation responsesexplicit (bool) cast by response cardShape(); key always present

HTTP 201 · Expected response

{
    "id": "card_demo",
    "card_type": "PHYSICAL",
    "brand": "MASTERCARD",
    "masked_pan": "**** **** **** 1234",
    "last4": "1234",
    "currency": "USD",
    "status": "ACTIVE",
    "atm_enabled": true
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Missing/mismatched _csrf. Security::requireCsrf() runs before Auth::requireUser().

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 422current_password_invalid / invalid_assoc_number / kyc_not_approved / physical_link_requires_sakura_driver / sakura_cards_scope_required / idempotency_key_too_long / active_merchant_membership_required / vrec_instance_required / sakura_user_not_linkedFormat: JSON

Step-up or card-link validation failed

HTTP 429too_many_attemptsFormat: JSON

More than 8 attempts in 10 minutes

HTTP 503physical_card_link_unavailableFormat: JSON

Provider temporarily unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • R4: Some SAKURA 422 responses remain V-REC 503; a repeated link can replay an earlier idempotency outcome. Contact operations for reconciliation.
  • Current code reads PHP $_POST (and $_FILES for KYC), not a JSON request body.
POST /api/v1/me/cards/{id}/{action} Change card status
Auth V-REC End-user Session Content-Type application/x-www-form-urlencoded OR multipart/form-data Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC card IDcard_...
actionstringyesCard actioncase-insensitive: activate|unfreeze=>ACTIVE, freeze=>FROZEN, close=>CLOSED

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionmust be a form/multipart field in $_POST; JSON body does not satisfy Security::requireCsrf()

OUT · Response contract

FieldTypePresentConstraint / meaning
idstringyesV-REC card public IDcards.public_id; card_...
card_typestringyesCard typeVIRTUAL|PHYSICAL
brandstring|nullyesCard brandcards.brand nullable
masked_panstring|nullyesMasked PANnever full PAN; nullable
last4string|nullyesLast four PAN digitsnullable
currencystringyesCard currencycards.currency
statusstringyesCard statusACTIVE|FROZEN|CLOSED (stored/provider-normalized)
atm_enabledbooleanyesATM flag explicitly boolean-cast in mutation responsesexplicit (bool) cast by response cardShape(); key always present

HTTP 200 · Expected response

{
    "id": "card_demo",
    "card_type": "VIRTUAL",
    "brand": "MASTERCARD",
    "masked_pan": "**** **** **** 1234",
    "last4": "1234",
    "currency": "USD",
    "status": "FROZEN",
    "atm_enabled": false
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Missing/mismatched _csrf. Security::requireCsrf() runs before Auth::requireUser().

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 422unsupported_action / card_not_found / card_close_not_supported_by_connected_app / vrec_instance_required / sakura_user_not_linked / sakura_cards_scope_required / invalid_card_statusFormat: JSON

Unsupported action or provider validation

HTTP 503card_status_unavailableFormat: JSON

Provider temporarily unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • With the current SAKURA card driver, the Connected App status API only permits ACTIVE/FROZEN. action=close is recognized by the V-REC controller but returns HTTP 422 card_close_not_supported_by_connected_app.
  • Current code reads PHP $_POST (and $_FILES for KYC), not a JSON request body.
POST /api/v1/me/cards/{id}/sensitive Temporarily retrieve PAN/expiry/CVV after end-user step-up
Auth V-REC End-user Session Content-Type application/x-www-form-urlencoded OR multipart/form-data Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC card IDcard_...

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionmust be a form/multipart field in $_POST; JSON body does not satisfy Security::requireCsrf()
current_passwordstringyesCurrent V-REC passwordstep-up verification

OUT · Response contract

FieldTypePresentConstraint / meaning
card_numberstringyesFull PAN; display transiently and do not persist
expirystringyesExpiry
cvvstringyesCVV; never persist

HTTP 200 · Expected response

{
    "card_number": "5555555555551234",
    "expiry": "12/29",
    "cvv": "123"
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Missing/mismatched _csrf. Security::requireCsrf() runs before Auth::requireUser().

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 403sakura_card_sensitive_scope_requiredFormat: JSON

SAKURA Connected App sensitive-card scope missing

HTTP 404card_not_foundFormat: JSON

Card not found for this user

HTTP 422current_password_invalid / card_sensitive_requires_sakura_driver / vrec_instance_required / sakura_user_not_linkedFormat: JSON

Step-up or validation failure

HTTP 429too_many_attemptsFormat: JSON

More than 6 attempts in 5 minutes

HTTP 503card_sensitive_unavailableFormat: JSON

Provider temporarily unavailable

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Responses are no-store. Never persist PAN/CVV in V-REC databases, logs or analytics.
  • Current code reads PHP $_POST (and $_FILES for KYC), not a JSON request body.
POST /api/v1/me/cards/{id}/pin Set or change a 4-digit physical card PIN
Auth V-REC End-user Session Content-Type application/x-www-form-urlencoded OR multipart/form-data Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC card IDcard_...

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionmust be a form/multipart field in $_POST; JSON body does not satisfy Security::requireCsrf()
current_passwordstringyesCurrent V-REC passwordstep-up verification
pinstringyesFour decimal digits for the new PIN^[0-9]{4}$; must match pin_confirmation
pin_confirmationstringyesRepeat the new PINexactly equals pin

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "success": true
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Missing/invalid CSRF form field

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No active V-REC user session

empty body + Location header
HTTP 404card_not_foundFormat: JSON

Card not found or not owned by the user

HTTP 422current_password_invalid / invalid_card_pin / physical_card_required / active_card_required / card_pin_requires_sakura_driver / sakura_card_sensitive_scope_required / matchmove_mobile_unavailableFormat: JSON

Password check failed, PIN invalid, card not physical/active, SAKURA connection missing or enrolled mobile unavailable

HTTP 429too_many_attemptsFormat: JSON

More than 4 attempts in 15 minutes

HTTP 503card_pin_unavailableFormat: JSON

SAKURA or issuer PIN setting failed or could not be confirmed

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • R3: 404 missing, 409 conflict, 422 business rejection, 503 transient upstream failure.
  • Requires a V-REC authenticated session and current V-REC password, NOT the SAKURA password. Submit form data, not JSON.
  • Requires SAKURA card_sensitive scope; physical and ACTIVE cards only. No PIN is returned or persisted. Response is no-store.
  • Not exposed to Merchant HMAC clients. If a PIN update times out, treat the result as unknown; do not retry blindly.
GET /api/v1/me/cards/{id}/issuer-transactions Read issuer transaction history for one owned card
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC card IDcard_...

Query parameters

FieldTypeRequiredConstraint / meaning
fromdatenoStart date (defaults to 30 days ago)YYYY-MM-DD, UTC
todatenoEnd date (defaults to today)YYYY-MM-DD; from <= to; max range 92 days; no later than UTC tomorrow
pageintegernoPage number1-100; default 1

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "items": [
        {
            "id": "txn_demo",
            "type": "purchase",
            "amount": "12.34",
            "currency": "USD",
            "description": "Demo merchant",
            "merchant": "Demo merchant",
            "status": "settled",
            "date": "2026-10-17"
        }
    ],
    "page": 1,
    "limit": 20,
    "from": "2026-09-17",
    "to": "2026-10-17",
    "total": 1,
    "has_more": false,
    "source": "matchmove"
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

Missing V-REC session

empty body + Location header
HTTP 404card_not_foundFormat: JSON

Card not found/not owned by user

HTTP 422invalid_page / invalid_date_range / sakura_cards_scope_requiredFormat: JSON

Invalid page or date range, or missing SAKURA cards scope

HTTP 429too_many_attemptsFormat: JSON

More than 30 attempts/minute

HTTP 502card_transactions_unavailableFormat: JSON

SAKURA/issuer unavailable or malformed response

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • R6: 404 for missing/non-owned local card; 502 if SAKURA ISSUED provenance cannot be reconciled.
  • Issuer card purchases only; differs from GET /api/v1/me/transactions (V-REC local records) and SGPUSD allocations.
  • Requires SAKURA cards scope, owned card, fixed limit 20. No-store. Not exposed through Merchant HMAC.
GET /api/v1/me/tenant-allocations List user tenant funding allocations
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

FieldTypePresentConstraint / meaning
data[]arrayyesUser-scoped tenant allocations (up to 100)
data[].public_idstringyesDirect allocation reference
data[].merchant_namestringyesFunding merchant label
data[].amountdecimal stringyesAmount in currency (not USDT)
data[].currencystringyesUSD
data[].statusstringyesCOMPLETED or pending/failed state
data[].remaining_amountdecimal string|nullyesRemaining source lot; null before completion

HTTP 200 · Expected response

{
    "data": [
        {
            "public_id": "vta_example",
            "merchant_name": "Example Tenant",
            "amount": "10.00000000",
            "currency": "USD",
            "status": "COMPLETED",
            "remaining_amount": "10.00000000",
            "created_at": "2026-10-17 02:29:51"
        }
    ]
}

Expected errors / HTTP responses

HTTP 302Location: /loginFormat: redirect

Authenticated V-REC user session required

Redirect to login

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Up to 100 most recent rows. Amounts remain in their original units.
GET /api/v1/me/tenant-returns List user tenant funding returns
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

FieldTypePresentConstraint / meaning
data[]arrayyesUser-scoped tenant returns (up to 100)
data[].public_idstringyesReturn reference
data[].amountdecimal stringyesAmount in currency
data[].currencystringyesUSD
data[].statusstringyesCOMPLETED or in-progress status

HTTP 200 · Expected response

{
    "data": []
}

Expected errors / HTTP responses

HTTP 302Location: /loginFormat: redirect

Authenticated V-REC user session required

Redirect to login

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Up to 100 most recent rows. Amounts remain in their original units.
GET /api/v1/me/ledger-entries List attributed V-REC funding ledger entries
Auth V-REC End-user Session Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

FieldTypePresentConstraint / meaning
data[]arrayyesAttributed V-REC funding ledger, max 100
data[].public_idstringyesLedger entry id
data[].entry_typestringyesCREDIT, DEBIT, TENANT_RETURN, ...
data[].amountdecimal stringyesSigned source ledger amount
data[].currencystringyesUSD; never treated as token units
data[].reference_typestringyesSource classification
data[].reference_idstringyesUnderlying event identifier

HTTP 200 · Expected response

{
    "data": [
        {
            "public_id": "le_example",
            "entry_type": "CREDIT",
            "amount": "10.00000000",
            "currency": "USD",
            "reference_type": "SAKURA_TENANT_ALLOCATION",
            "reference_id": "sakura_request_example"
        }
    ]
}

Expected errors / HTTP responses

HTTP 302Location: /loginFormat: redirect

Authenticated V-REC user session required

Redirect to login

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Up to 100 most recent rows. Amounts remain in their original units.
BASE URL

Crypto Wallet (end-user session)

https://v-rec.xyz
GET /api/v1/me/wallet Wallet summary, USDT/ETH balances and gasless eligibility SAKURA crypto_wallet
Auth V-REC End-user Session Scope SAKURA crypto_wallet Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Query parameters

FieldTypeRequiredConstraint / meaning
skip_balancesanynoWhen present, skip on-chain balance reads (faster)

OUT · Response contract

FieldTypePresentConstraint / meaning
enabledbooleanyesWhether the SAKURA-side V-REC wallet feature is enabled
access.allowedbooleanyesWhether this user may create/transfer (SAKURA display mode / per-user enablement)
access.reasonstring|nullyesReason when not allowednot_enabled_for_user | disabled_by_operator | service_disabled
features.wallet_creation / wallet_rotation / gasless_transferbooleanyesSAKURA feature flags
has_walletbooleanyesWhether an active wallet exists
wallet.addressstringconditionalReceiving address (Ethereum / ERC-20)^0x[0-9a-f]{40}$
wallet.versionintegerconditionalIncrements on every rotation
wallet.key_schemestringconditionalKey schemebip39_bip44 | legacy_random
wallet.derivation_pathstring|nullconditionalMetaMask-compatible derivation pathm/44'/60'/0'/0/0
balances.usdt.balancedecimal-string|nullconditionalOn-chain USDT balance (6 dp); null if RPC failed
balances.usdt.pending_incomingdecimal-stringconditionalUnconfirmed incoming USDT
balances.usdt.availabledecimal-string|nullconditionalSendable = balance − pending_incoming (floored at 0)
balances.eth.balancedecimal-string|nullconditionalETH balance (18 dp); not required for gasless transfers
balances.completebooleanconditionalWhether every balance was read
gasless.allowedbooleanyesWhether gasless transfer is available now
gasless.reasonstring|nullyesEligibility reasonmerchant_managed | physical_card_exempt | not_subscriber | card_suspended | past_due | overdue | transfer_blocked ...
gasless.messagestring|nullyesUser-facing explanation (ja/en)
gasless.daily_limit / daily_remainingintegeryesDaily attempt limit and remaining (failures count)
transfer_blockedbooleanyesTransfers suspended by the operator
previous_wallets[]array<object>yesRotated wallets (address, version, deactivated_at, deactivate_reason)

HTTP 200 · Expected response

{
    "enabled": true,
    "access": {
        "allowed": true,
        "reason": null
    },
    "features": {
        "wallet_creation": true,
        "wallet_rotation": true,
        "gasless_transfer": true
    },
    "has_wallet": true,
    "wallet": {
        "address": "0x3f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be",
        "version": 1,
        "key_scheme": "bip39_bip44",
        "derivation_path": "m/44'/60'/0'/0/0",
        "seed_phrase_wallet": true,
        "network": "ethereum",
        "chain_id": 1,
        "created_at": "2026-10-16T01:00:00+00:00"
    },
    "balances": {
        "usdt": {
            "symbol": "USDT",
            "contract": "0xdac17f958d2ee523a2206206994597c13d831ec7",
            "decimals": 6,
            "balance": "120.500000",
            "pending_incoming": "0.000000",
            "available": "120.500000"
        },
        "eth": {
            "symbol": "ETH",
            "balance": "0.000000000000000000"
        },
        "complete": true,
        "fetched_at": "2026-10-16T01:02:03+00:00"
    },
    "gasless": {
        "allowed": true,
        "reason": "merchant_managed",
        "message": null,
        "past_due_months": [],
        "exempt_until": null,
        "asset": "USDT",
        "network": "ethereum",
        "daily_limit": 5,
        "daily_remaining": 4
    },
    "transfer_blocked": false,
    "previous_wallets": []
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 403wallet_not_availableFormat: JSON

SAKURA Connected App crypto_wallet (crypto_wallet_manage for writes) scope not granted

HTTP 503wallet_service_unavailableFormat: JSON

Temporary SAKURA / blockchain RPC failure; includes correlation_id

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Responses are Cache-Control: no-store.
  • Balances are read live from an Ethereum node by SAKURA; values are decimal strings.
GET /api/v1/me/wallet/transfers Transfer history and in-flight requests SAKURA crypto_wallet
Auth V-REC End-user Session Scope SAKURA crypto_wallet Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Query parameters

FieldTypeRequiredConstraint / meaning
limitintegernoRows1..100, default 20

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "data": [
        {
            "transfer_id": 41,
            "status": "SUBMITTED",
            "fee_mode": "gasless",
            "symbol": "USDT",
            "amount": "10.000000",
            "from_address": "0x3f5c...",
            "to_address": "0x9b2e...",
            "tx_hash": "0xabc...",
            "explorer_url": "https://etherscan.io/tx/0xabc...",
            "created_at": "2026-10-16T01:10:00+00:00"
        }
    ],
    "requests": [
        {
            "public_id": "wrq_...",
            "operation": "gasless_transfer",
            "idempotency_key": "vwt_...",
            "status": "SUCCEEDED",
            "to_address": "0x9b2e...",
            "amount": "10",
            "tx_hash": "0xabc..."
        }
    ]
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 403wallet_not_availableFormat: JSON

SAKURA Connected App crypto_wallet (crypto_wallet_manage for writes) scope not granted

HTTP 503wallet_service_unavailableFormat: JSON

Temporary SAKURA / blockchain RPC failure; includes correlation_id

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

POST /api/v1/me/wallet/address-check Pre-transfer destination safety check (address poisoning) SAKURA crypto_wallet
Auth V-REC End-user Session Scope SAKURA crypto_wallet Content-Type application/x-www-form-urlencoded Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionform field ($_POST); JSON body does not satisfy Security::requireCsrf()
addressstringyesDestination addressmax 64

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "address": "0x9b2e...",
    "verdict": "danger",
    "risk_score": 85,
    "checksum_valid": true,
    "lookalike": "0x9b2e...1f",
    "findings": [
        {
            "severity": "danger",
            "message": "過去の送金先と先頭・末尾が一致する別アドレスです。"
        }
    ],
    "requires_acknowledgement": true
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Missing/mismatched _csrf

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 403wallet_not_availableFormat: JSON

SAKURA Connected App crypto_wallet (crypto_wallet_manage for writes) scope not granted

HTTP 422invalid_addressFormat: JSON

Missing/too long

HTTP 429too_many_attemptsFormat: JSON

More than 60 per 10 minutes

HTTP 503wallet_service_unavailableFormat: JSON

Temporary SAKURA / blockchain RPC failure; includes correlation_id

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Sending to a verdict=danger destination requires risk_acknowledged=1 on the transfer (or is refused if SAKURA disallows overrides).
POST /api/v1/me/wallet Create wallet (returns the seed phrase once) SAKURA crypto_wallet_manage
Auth V-REC End-user Session Scope SAKURA crypto_wallet_manage Content-Type application/x-www-form-urlencoded Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-Keyheader stringnoPrevents duplicate creation on retry^[A-Za-z0-9._:-]{1,120}$

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionform field ($_POST); JSON body does not satisfy Security::requireCsrf()
current_passwordstringyesV-REC sign-in password (step-up)
wallet_passwordstringyesNew wallet password (needed for every transfer)10..512 chars; should differ from the V-REC password
wallet_password_confirmationstringyesConfirmationmust equal wallet_password

OUT · Response contract

FieldTypePresentConstraint / meaning
operation_idstringyesSAKURA operation idvwo_...
operationstringyesOperationcreate | rotate
replayedbooleanyestrue when a stored result is returned for a retried idempotency key
statusstringyesResultCREATED | ROTATED
walletobjectyesNew wallet (same shape as wallet_summary.wallet)
previous_addressstringrotate onlyAddress before rotation
mnemonicstring|nullyes12-word BIP-39 seed phrase, returned once only; null when replayed=trueNEVER log, store, cache or send to analytics
mnemonic_availablebooleanyesWhether mnemonic is present

HTTP 201 · Expected response

{
    "operation_id": "vwo_...",
    "idempotency_key": "vwc_...",
    "operation": "create",
    "replayed": false,
    "status": "CREATED",
    "wallet": {
        "address": "0x3f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be",
        "version": 1,
        "key_scheme": "bip39_bip44",
        "derivation_path": "m/44'/60'/0'/0/0",
        "seed_phrase_wallet": true,
        "network": "ethereum",
        "chain_id": 1,
        "created_at": "2026-10-16T01:00:00+00:00"
    },
    "mnemonic": "word1 word2 ... word12",
    "mnemonic_words": 12,
    "mnemonic_shown_once": true,
    "mnemonic_available": true
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Missing/mismatched _csrf

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 403wallet_not_availableFormat: JSON

SAKURA Connected App crypto_wallet (crypto_wallet_manage for writes) scope not granted

HTTP 403wallet_access_denied / wallet_creation_disabledFormat: JSON

User or feature not enabled

HTTP 409wallet_already_existsFormat: JSON

An active wallet already exists (use rotate)

HTTP 422current_password_invalid / wallet_password_invalid_formatFormat: JSON

Step-up failed / password rule

HTTP 429too_many_attemptsFormat: JSON

More than 8 step-ups per 10 minutes

HTTP 503wallet_creation_unavailable / wallet_service_unavailableFormat: JSON

e.g. SAKURA recovery key not configured

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Responses are Cache-Control: no-store.
  • Never persist the mnemonic in databases, logs or analytics. The V-REC web UI keeps it APP_KEY-encrypted in the session for at most 5 minutes and discards it after display.
POST /api/v1/me/wallet/rotate Rotate wallet SAKURA crypto_wallet_manage
Auth V-REC End-user Session Scope SAKURA crypto_wallet_manage Content-Type application/x-www-form-urlencoded Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-Keyheader stringnoPrevents a double rotation on retry^[A-Za-z0-9._:-]{1,120}$

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionform field ($_POST); JSON body does not satisfy Security::requireCsrf()
current_passwordstringyesV-REC sign-in password (step-up)
new_wallet_passwordstringyesPassword for the new wallet10..512 chars
new_wallet_password_confirmationstringyesConfirmation
reasonstringyesReasoncompromised | precaution | other

OUT · Response contract

FieldTypePresentConstraint / meaning
operation_idstringyesSAKURA operation idvwo_...
operationstringyesOperationcreate | rotate
replayedbooleanyestrue when a stored result is returned for a retried idempotency key
statusstringyesResultCREATED | ROTATED
walletobjectyesNew wallet (same shape as wallet_summary.wallet)
previous_addressstringrotate onlyAddress before rotation
mnemonicstring|nullyes12-word BIP-39 seed phrase, returned once only; null when replayed=trueNEVER log, store, cache or send to analytics
mnemonic_availablebooleanyesWhether mnemonic is present

HTTP 201 · Expected response

{
    "operation_id": "vwo_...",
    "operation": "rotate",
    "replayed": false,
    "status": "ROTATED",
    "reason": "compromised",
    "wallet": {
        "address": "0x51e1...",
        "version": 2,
        "key_scheme": "bip39_bip44",
        "derivation_path": "m/44'/60'/0'/0/0",
        "seed_phrase_wallet": true,
        "network": "ethereum",
        "chain_id": 1,
        "created_at": "2026-10-16T01:00:00+00:00"
    },
    "previous_address": "0x3f5c...",
    "mnemonic": "word1 ... word12",
    "mnemonic_available": true
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Missing/mismatched _csrf

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 403wallet_not_availableFormat: JSON

SAKURA Connected App crypto_wallet (crypto_wallet_manage for writes) scope not granted

HTTP 404wallet_not_foundFormat: JSON

Nothing to rotate (create first)

HTTP 422invalid_reason / current_password_invalid / wallet_password_invalid_formatFormat: JSON

Validation

HTTP 429too_many_attemptsFormat: JSON

Step-up limit

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Responses are Cache-Control: no-store.
  • The old wallet is deactivated in the same transaction. Balances on the old address are NOT moved automatically; move them with the old seed phrase in an external wallet.
POST /api/v1/me/wallet/gasless-transfer USDT gasless transfer (gas paid by SAKURA) SAKURA crypto_wallet_manage
Auth V-REC End-user Session Scope SAKURA crypto_wallet_manage Content-Type application/x-www-form-urlencoded Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-Keyheader stringyesUnique per transfer; reuse only to retry the same transfer^[A-Za-z0-9._:-]{1,120}$

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionform field ($_POST); JSON body does not satisfy Security::requireCsrf()
to_addressstringyesDestination^0x[0-9a-fA-F]{40}$
amountdecimal-stringyesUSDT amount^\d{1,15}(\.\d{1,6})?$, > 0
wallet_passwordstringyesWallet password (SAKURA decrypts the key to sign)
risk_acknowledgedboolean-stringnoAcknowledge a dangerous destination1 | true

OUT · Response contract

FieldTypePresentConstraint / meaning
operation_idstringyesSAKURA operation idvwo_...
statusstringyesBroadcastSUBMITTED
transfer_idintegeryesSAKURA transfer log id
fee_modestringyesGas payergasless
symbol / amountstring / decimal-stringyesAsset and amount (6 dp)USDT
from_address / to_addressstringyesFrom / to^0x[0-9a-f]{40}$
tx_hash / explorer_urlstringyesTransaction hash and Etherscan URL
daily_remainingintegeryesRemaining attempts today
request_keystringyesKey for status lookups (/api/v1/me/wallet/requests/{key})

HTTP 201 · Expected response

{
    "operation_id": "vwo_...",
    "idempotency_key": "vwt_...",
    "operation": "gasless_transfer",
    "replayed": false,
    "status": "SUBMITTED",
    "transfer_id": 41,
    "fee_mode": "gasless",
    "symbol": "USDT",
    "amount": "10.000000",
    "from_address": "0x3f5c...",
    "to_address": "0x9b2e...",
    "tx_hash": "0xabc...",
    "explorer_url": "https://etherscan.io/tx/0xabc...",
    "daily_remaining": 3,
    "request_key": "vwt_..."
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Missing/mismatched _csrf

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 403wallet_not_availableFormat: JSON

SAKURA Connected App crypto_wallet (crypto_wallet_manage for writes) scope not granted

HTTP 202transfer_status_unknownFormat: JSON

HTTP to SAKURA timed out; the transfer may still be processing. Do not resend — poll status_url

HTTP 403gasless_not_allowed / transfer_blocked / wallet_access_deniedFormat: JSON

Not a subscriber, past due, suspended by operator, etc. (with reason)

HTTP 404wallet_not_foundFormat: JSON

No wallet

HTTP 409destination_risk / operation_in_progress / idempotency_key_reusedFormat: JSON

Dangerous destination (needs risk_acknowledged) / same key in progress / same key with different body

HTTP 422wallet_password_invalid / insufficient_balance / invalid_amount / invalid_destination / idempotency_key_requiredFormat: JSON

Input or balance error

HTTP 429wallet_password_locked / daily_limit_reached / too_many_attemptsFormat: JSON

Password lockout (30 min) / daily limit / V-REC rate limit

HTTP 502wallet_operation_failedFormat: JSON

On-chain processing failed; with correlation_id. Check history, then retry with a NEW key

HTTP 503wallet_service_unavailableFormat: JSON

Temporary SAKURA / blockchain RPC failure; includes correlation_id

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Responses are Cache-Control: no-store.
  • SAKURA's relayer funds gas, then approve → transferFrom. The user needs no ETH.
  • May take up to 150 s while waiting for confirmations. Timeouts never cause a double transfer (SAKURA de-duplicates by key).
GET /api/v1/me/wallet/requests/{key} Look up a wallet request status SAKURA crypto_wallet
Auth V-REC End-user Session Scope SAKURA crypto_wallet Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Path parameters

FieldTypeRequiredConstraint / meaning
keystringyesrequest_key from the transfer or transfer_status_unknown response^[A-Za-z0-9._:-]{1,190}$

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "operation_id": "vwo_...",
    "idempotency_key": "vwt_...",
    "operation": "gasless_transfer",
    "status": "SUCCEEDED",
    "error": null,
    "created_at": "2026-10-16T01:10:00+00:00",
    "updated_at": "2026-10-16T01:11:30+00:00",
    "result": {
        "status": "SUBMITTED",
        "tx_hash": "0xabc..."
    }
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No valid V-REC end-user session. This is not a JSON error body.

empty body + Location header
HTTP 404operation_not_foundFormat: JSON

Never reached SAKURA (safe to retry with a NEW key)

HTTP 503wallet_service_unavailableFormat: JSON

Temporary SAKURA / blockchain RPC failure; includes correlation_id

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • status: PROCESSING | SUCCEEDED | FAILED. If PROCESSING persists, transfer shows the SAKURA relay log state.
BASE URL

Purchases paid in crypto (sale contracts, end-user session)

https://v-rec.xyz
GET /api/v1/me/wallet/purchase-catalog Products, payment assets, sellers and fees SAKURA crypto_wallet
Auth V-REC End-user Session Scope SAKURA crypto_wallet Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "enabled": true,
    "products": [
        {
            "code": "SGP_USD"
        },
        {
            "code": "POINT",
            "name": "ショップpt",
            "points_per_usd": "100"
        }
    ],
    "payment_assets": [
        {
            "code": "USDT",
            "seller": {
                "SGP_USD": "SAKURA_EXCHANGE",
                "POINT": "MERCHANT"
            }
        },
        {
            "code": "CUSTOM_TOKEN",
            "symbol": "SHOP",
            "unit_price_usd": "0.5",
            "merchant_responsibility": true
        }
    ],
    "fees": {
        "company_fee_rate": "0.01",
        "merchant_royalty_rate": "0.005",
        "total_rate": "0.015",
        "standard_route_rate": "0.02"
    }
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No end-user session

redirect
HTTP 403wallet_not_availableFormat: JSON

SAKURA crypto_wallet / crypto_wallet_manage scope not granted

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • enabled=true only when the merchant enabled V-REC sales in the SAKURA merchant console.
POST /api/v1/me/wallet/purchase-quotes Quote (fix the sale terms) SAKURA crypto_wallet_manage
Auth V-REC End-user Session Scope SAKURA crypto_wallet_manage Content-Type application/x-www-form-urlencoded Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Headers

FieldTypeRequiredConstraint / meaning
Idempotency-Keyheader stringyesSame contract on retry^[A-Za-z0-9._:-]{1,120}$

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionform field
productstringyesGoodsSGP_USD | POINT
payment_assetstringyesPayment assetUSDT | CUSTOM_TOKEN
payment_amountdecimal-stringyesPayment amount> 0

OUT · Response contract

FieldTypePresentConstraint / meaning
contract_nostringyesSale contract numberVCS-...
contract_typestringyesAlways SALE (not an exchange)
statusstringyesStatusQUOTED | PAYMENT_PROCESSING | PAYMENT_SUBMITTED | COMPLETED | FAILED | EXPIRED | MANUAL_REVIEW
product / product_name / product_quantitystringyesGoods (SGP_USD or POINT) and quantity
sellerstringyesSeller: SAKURA_EXCHANGE only for SGP-USD paid in USDT; otherwise MERCHANTSAKURA_EXCHANGE | MERCHANT
payment.asset / symbol / amount / unit_price_usdstringyesPrice paid in crypto and unit price
payment.pay_to_addressstringyesPay-to: company sales address for USDT; merchant address for custom token
pricing.gross_usddecimal-stringyesUSD value of the price (floor 6)
pricing.company_fee_rate / company_fee_usddecimal-stringyesCompany fee (1%; standard route 2%)
pricing.merchant_royalty_rate / merchant_royalty_usddecimal-stringyesMerchant royalty (set by the merchant, added on top)
pricing.net_usddecimal-stringyesGoods value = gross − fee − royalty
merchant_responsibilitybooleanyesCustom-token payment, accepted solely under the merchant's responsibility
termsobjectdetail onlyTerms snapshot at quote time (ja/en)

HTTP 201 · Expected response

{
    "contract_no": "VCS-20261016-AB12CD34EF",
    "contract_type": "SALE",
    "status": "QUOTED",
    "product": "SGP_USD",
    "product_name": "SGP-USD",
    "product_quantity": "98.500000",
    "seller": "SAKURA_EXCHANGE",
    "payment": {
        "asset": "USDT",
        "symbol": "USDT",
        "amount": "100.000000",
        "unit_price_usd": "1.00000000",
        "pay_to_address": "0x...",
        "tx_hash": null
    },
    "pricing": {
        "gross_usd": "100.000000",
        "company_fee_rate": "0.01000000",
        "company_fee_usd": "1.000000",
        "merchant_royalty_rate": "0.00500000",
        "merchant_royalty_usd": "0.500000",
        "net_usd": "98.500000",
        "points_per_usd": null
    },
    "merchant_responsibility": false,
    "quote_expires_at": "2026-10-16T01:10:00+00:00",
    "terms": {
        "version": "2026-10-16",
        "seller_name": "SAKURA EXCHANGE GUAM INC.",
        "ja": [
            "本契約は、購入者が暗号資産(USDT)を代金として、売主から「SGP-USD」を購入する売買契約です。通貨の両替ではありません。"
        ],
        "en": [
            "This is a sale contract …"
        ]
    },
    "replayed": false
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Bad _csrf

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No end-user session

redirect
HTTP 403wallet_not_availableFormat: JSON

SAKURA crypto_wallet / crypto_wallet_manage scope not granted

HTTP 403product_not_offered / payment_asset_not_accepted / merchant_sales_not_enabledFormat: JSON

Not offered

HTTP 409merchant_pool_insufficient / idempotency_key_reusedFormat: JSON

Merchant pool insufficient etc.

HTTP 422invalid_amount / amount_out_of_range / amount_too_small / idempotency_key_requiredFormat: JSON

Input error

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Quotes expire after 10 minutes by default.
POST /api/v1/me/wallet/purchases/{contract}/confirm Accept terms and pay SAKURA crypto_wallet_manage
Auth V-REC End-user Session Scope SAKURA crypto_wallet_manage Content-Type application/x-www-form-urlencoded Success HTTP 201

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Path parameters

FieldTypeRequiredConstraint / meaning
contractstringyesContract numberVCS-...

Request body

FieldTypeRequiredConstraint / meaning
_csrfstringyesCSRF token bound to the V-REC sessionform field
accept_termsboolean-stringyesAcceptance1 | true
wallet_passwordstringyesWallet password

OUT · Response contract

FieldTypePresentConstraint / meaning
contract_nostringyesSale contract numberVCS-...
contract_typestringyesAlways SALE (not an exchange)
statusstringyesStatusQUOTED | PAYMENT_PROCESSING | PAYMENT_SUBMITTED | COMPLETED | FAILED | EXPIRED | MANUAL_REVIEW
product / product_name / product_quantitystringyesGoods (SGP_USD or POINT) and quantity
sellerstringyesSeller: SAKURA_EXCHANGE only for SGP-USD paid in USDT; otherwise MERCHANTSAKURA_EXCHANGE | MERCHANT
payment.asset / symbol / amount / unit_price_usdstringyesPrice paid in crypto and unit price
payment.pay_to_addressstringyesPay-to: company sales address for USDT; merchant address for custom token
pricing.gross_usddecimal-stringyesUSD value of the price (floor 6)
pricing.company_fee_rate / company_fee_usddecimal-stringyesCompany fee (1%; standard route 2%)
pricing.merchant_royalty_rate / merchant_royalty_usddecimal-stringyesMerchant royalty (set by the merchant, added on top)
pricing.net_usddecimal-stringyesGoods value = gross − fee − royalty
merchant_responsibilitybooleanyesCustom-token payment, accepted solely under the merchant's responsibility
termsobjectdetail onlyTerms snapshot at quote time (ja/en)

HTTP 201 · Expected response

{
    "contract_no": "VCS-...",
    "status": "PAYMENT_SUBMITTED",
    "payment": {
        "tx_hash": "0xabc..."
    },
    "replayed": false
}

Expected errors / HTTP responses

HTTP 419CSRF validation failedFormat: non-JSON

Bad _csrf

CSRF validation failed
HTTP 302Location: /v/{instance}/loginFormat: redirect

No end-user session

redirect
HTTP 202payment_status_unknownFormat: JSON

HTTP timeout to SAKURA; poll status_url, do not resend

HTTP 409quote_expired / wallet_changed / merchant_collateral_shortfallFormat: JSON

Expired etc.

HTTP 422terms_not_accepted / wallet_password_invalid / insufficient_balanceFormat: JSON

Not accepted / wrong password (quote stays retryable)

HTTP 429wallet_password_locked / too_many_attemptsFormat: JSON

Lock etc.

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Paid gaslessly (company pays gas); no subscription required. Never pays twice. Goods delivered after 12 confirmations (COMPLETED).
GET /api/v1/me/wallet/purchases List sale contracts SAKURA crypto_wallet
Auth V-REC End-user Session Scope SAKURA crypto_wallet Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Query parameters

FieldTypeRequiredConstraint / meaning
limitintegernoRows1..100

OUT · Response contract

No body or no endpoint-specific response schema.

HTTP 200 · Expected response

{
    "data": [
        {
            "contract_no": "VCS-...",
            "status": "COMPLETED"
        }
    ]
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No end-user session

redirect
HTTP 403wallet_not_availableFormat: JSON

SAKURA crypto_wallet / crypto_wallet_manage scope not granted

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

GET /api/v1/me/wallet/purchases/{contract} Get a sale contract SAKURA crypto_wallet
Auth V-REC End-user Session Scope SAKURA crypto_wallet Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
V-REC session cookiecookieyesAuthenticated session cookie configured by SESSION_NAMEdefault configuration name: vrec_gateway; runtime-configurable

Path parameters

FieldTypeRequiredConstraint / meaning
contractstringyesContract numberVCS-...

OUT · Response contract

FieldTypePresentConstraint / meaning
contract_nostringyesSale contract numberVCS-...
contract_typestringyesAlways SALE (not an exchange)
statusstringyesStatusQUOTED | PAYMENT_PROCESSING | PAYMENT_SUBMITTED | COMPLETED | FAILED | EXPIRED | MANUAL_REVIEW
product / product_name / product_quantitystringyesGoods (SGP_USD or POINT) and quantity
sellerstringyesSeller: SAKURA_EXCHANGE only for SGP-USD paid in USDT; otherwise MERCHANTSAKURA_EXCHANGE | MERCHANT
payment.asset / symbol / amount / unit_price_usdstringyesPrice paid in crypto and unit price
payment.pay_to_addressstringyesPay-to: company sales address for USDT; merchant address for custom token
pricing.gross_usddecimal-stringyesUSD value of the price (floor 6)
pricing.company_fee_rate / company_fee_usddecimal-stringyesCompany fee (1%; standard route 2%)
pricing.merchant_royalty_rate / merchant_royalty_usddecimal-stringyesMerchant royalty (set by the merchant, added on top)
pricing.net_usddecimal-stringyesGoods value = gross − fee − royalty
merchant_responsibilitybooleanyesCustom-token payment, accepted solely under the merchant's responsibility
termsobjectdetail onlyTerms snapshot at quote time (ja/en)

HTTP 200 · Expected response

{
    "contract_no": "VCS-20261016-AB12CD34EF",
    "contract_type": "SALE",
    "status": "COMPLETED",
    "product": "SGP_USD",
    "product_name": "SGP-USD",
    "product_quantity": "98.500000",
    "seller": "SAKURA_EXCHANGE",
    "payment": {
        "asset": "USDT",
        "symbol": "USDT",
        "amount": "100.000000",
        "unit_price_usd": "1.00000000",
        "pay_to_address": "0x...",
        "tx_hash": null
    },
    "pricing": {
        "gross_usd": "100.000000",
        "company_fee_rate": "0.01000000",
        "company_fee_usd": "1.000000",
        "merchant_royalty_rate": "0.00500000",
        "merchant_royalty_usd": "0.500000",
        "net_usd": "98.500000",
        "points_per_usd": null
    },
    "merchant_responsibility": false,
    "quote_expires_at": "2026-10-16T01:10:00+00:00",
    "terms": {
        "version": "2026-10-16",
        "seller_name": "SAKURA EXCHANGE GUAM INC.",
        "ja": [
            "本契約は、購入者が暗号資産(USDT)を代金として、売主から「SGP-USD」を購入する売買契約です。通貨の両替ではありません。"
        ],
        "en": [
            "This is a sale contract …"
        ]
    },
    "replayed": false
}

Expected errors / HTTP responses

HTTP 302Location: /v/{instance}/loginFormat: redirect

No end-user session

redirect
HTTP 404sale_not_foundFormat: JSON

Not found

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

BASE URL

Merchant Learning & Custom Token Pool API

/api/merchant/v1
GET /api/merchant/v1/learning/programs List up to 100 published courses belonging to this merchant learning.read
Auth Merchant HMAC Scope learning.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

OUT · Response contract

FieldTypePresentConstraint / meaning
dataarrayyes公開済み講座(最大100件) / Merchant-published programs公開済み講座(最大100件) / Merchant-published programs

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 講座の公開ステータスと加盟店はSAKURAが検証します。
  • V-REC HMAC = learning.read / SAKURA Connected Apps = learning_read
GET /api/merchant/v1/learning/programs/{id} Merchant course details and enrollment conditions learning.read
Auth Merchant HMAC Scope learning.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idintegeryesProgram ID>0

OUT · Response contract

FieldTypePresentConstraint / meaning
dataarrayyes公開済み講座(最大100件) / Merchant-published programs公開済み講座(最大100件) / Merchant-published programs

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Private Meet/recording URLs and quiz answers are omitted
POST /api/merchant/v1/learning/programs/{id}/enrollments Consent-based free-course enrollment learning.enroll.write
Auth Merchant HMAC Scope learning.enroll.write Content-Type application/json Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idintegeryesProgram ID>0

Request body

FieldTypeRequiredConstraint / meaning
user_idstringyesV-REC user public IDusr_...
learner_consentbooleanyesMember consenttrue
consent_referencestringyesMerchant consent reference8..100 characters

OUT · Response contract

FieldTypePresentConstraint / meaning
dataarrayyes公開済み講座(最大100件) / Merchant-published programs公開済み講座(最大100件) / Merchant-published programs

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • SAKURA learning_enroll scope, matching membership; paid course returns 422
GET /api/merchant/v1/learning/programs/{id}/sessions Get lesson schedule and completion rules learning.read
Auth Merchant HMAC Scope learning.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idintegeryesSAKURA program ID (positive integer)SAKURA program ID (positive integer)

OUT · Response contract

FieldTypePresentConstraint / meaning
programobjectyes対象講座 / Course対象講座 / Course
sessionsarrayyesキャンセル除外の授業 / Non-cancelled lessonsキャンセル除外の授業 / Non-cancelled lessons

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 認証済み加盟店以外の講座は404です。
  • V-REC HMAC = learning.read / SAKURA Connected Apps = learning_read
GET /api/merchant/v1/learning/users/{id}/schedule Read member course schedule learning.read
Auth Merchant HMAC Scope learning.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC usr_... ID (ACTIVE/FULL)V-REC usr_... ID (ACTIVE/FULL)

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC public user ID (usr_...)V-REC usr_...
dataarrayyes加入済み講座・授業 / Enrolled courses and sessions加入済み講座・授業 / Enrolled courses and sessions

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 他の加盟店の会員IDを指定しても参照できません。
  • V-REC HMAC = learning.read / SAKURA Connected Apps = learning_read
GET /api/merchant/v1/learning/users/{id}/progress Read member attendance progress and Google Calendar add URLs learning.read
Auth Merchant HMAC Scope learning.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC usr_... ID (ACTIVE/FULL)V-REC usr_... ID (ACTIVE/FULL)

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC usr_... member IDV-REC usr_... member ID
google_calendar_connectedbooleanyesGoogle連携設定 / Google authorization statusGoogle連携設定 / Google authorization status
dataarrayyes授業毎のcompleted_at, completion_status, duration_seconds, calendar_sync_status, google_calendar_add_url授業毎のcompleted_at, completion_status, duration_seconds, calendar_sync_status, google_calendar_add_url

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Google CalendarのURLはクリックによる保存画面であり自動登録ではありません。
  • V-REC HMAC = learning.read / SAKURA Connected Apps = learning_read
GET /api/merchant/v1/learning/rewards Read recent custom token learning rewards learning.read
Auth Merchant HMAC Scope learning.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Query parameters

FieldTypeRequiredConstraint / meaning
session_idintegernooptional positive IDoptional positive ID
program_idintegernooptional positive IDoptional positive ID
limitintegerno1–100; default 501–100; default 50

OUT · Response contract

FieldTypePresentConstraint / meaning
dataarrayyesUp to 100 rows. Each user_id is V-REC usr_...; internal SAKURA user_ref is omitted.カスタムトークン付与履歴(最大100件) / Last up to 100 reward rows

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 加盟店配下の受講報酬を返します。履歴取得は最大100件で全期間集計ではありません。
  • V-REC HMAC = learning.read / SAKURA Connected Apps = learning_read
POST /api/merchant/v1/learning/sessions/{id}/complete Submit external lesson completion evidence learning.rewards.write
Auth Merchant HMAC Scope learning.rewards.write Content-Type application/json Success HTTP 202

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idintegeryesSAKURA session IDSAKURA session ID

Request body

FieldTypeRequiredConstraint / meaning
user_idstringyesV-REC usr_... IDV-REC usr_... ID
idempotency_keystringyes8–120 chars [A-Za-z0-9._:-]8–120 chars [A-Za-z0-9._:-]
evidence_refstringyes8–190 chars; auditable provider evidence8–190 chars; auditable provider evidence
duration_secondsintegeryes1–864001–86400

OUT · Response contract

FieldTypePresentConstraint / meaning
session_idintegeryes授業ID / Lesson ID授業ID / Lesson ID
attendance_statusstringyes再判定後の出席ステータス / Assessed attendance再判定後の出席ステータス / Assessed attendance
rewardobject|nullconditional付与台帳の状態。nullの場合もある / May be null付与台帳の状態。nullの場合もある / May be null

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

HTTP 422validation_errorFormat: JSON

Invalid fields, eligibility, balance or wallet

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 授業終了後のみ受付。出席時間・小テスト・アンケート等の既存条件で受講完了を判定し、202は報酬確定を保証しません。
  • V-REC HMAC = learning.rewards.write / SAKURA Connected Apps = learning_rewards
POST /api/merchant/v1/learning/sessions/{id}/rewards Retry configured reward for completed lesson learning.rewards.write
Auth Merchant HMAC Scope learning.rewards.write Content-Type application/json Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idintegeryesSAKURA session IDSAKURA session ID

Request body

FieldTypeRequiredConstraint / meaning
user_idstringyesV-REC usr_... IDV-REC usr_... ID
idempotency_keystringyes8–120 chars [A-Za-z0-9._:-]8–120 chars [A-Za-z0-9._:-]

OUT · Response contract

FieldTypePresentConstraint / meaning
rewardobjectyes既存設定のトークン付与結果 / Existing configured grant record既存設定のトークン付与結果 / Existing configured grant record

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

HTTP 422validation_errorFormat: JSON

Invalid fields, eligibility, balance or wallet

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 受講完了を確認後、既存台帳を利用。任意額・トークン指定は不可。
  • V-REC HMAC = learning.rewards.write / SAKURA Connected Apps = learning_rewards
POST /api/merchant/v1/learning/users/{id}/calendar-sync Sync enrolled lessons to approved Google Calendar learning.calendar.write
Auth Merchant HMAC Scope learning.calendar.write Content-Type application/json Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesV-REC usr_... IDV-REC usr_... ID

Request body

FieldTypeRequiredConstraint / meaning
program_idintegeryespositive integer, enrolled programpositive integer, enrolled program

OUT · Response contract

FieldTypePresentConstraint / meaning
user_idstringyesV-REC member IDV-REC member ID
syncobjectyes同期件数・失敗数 / Sync and failure counts同期件数・失敗数 / Sync and failure counts

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

HTTP 422validation_errorFormat: JSON

Invalid fields, eligibility, balance or wallet

HTTP 207partial_calendar_syncFormat: JSON

Partial calendar sync failure

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • Google OAuth同意済み会員のみ。未承認は409、部分失敗207、1講座20授業まで。
  • V-REC HMAC = learning.calendar.write / SAKURA Connected Apps = learning_calendar
GET /api/merchant/v1/token-pool Read dedicated ERC-20 pool and all-time ledger statistics learning.token_pool.read
Auth Merchant HMAC Scope learning.token_pool.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

OUT · Response contract

FieldTypePresentConstraint / meaning
contractstringyesERC-20 contractERC-20 contract
symbolstringyesToken symbolToken symbol
onchain_balancestring|nullconditionalオンチェーン残高取得結果 / Chain balance when availableオンチェーン残高取得結果 / Chain balance when available
reserved_amountstringyes予約済み数量 / Reserved amount予約済み数量 / Reserved amount
available_estimatestring|nullconditional参考利用可能量(確定残高ではない) / Estimated available参考利用可能量(確定残高ではない) / Estimated available
statisticsobjectyes全期間集計 / All-time totals全期間集計 / All-time totals

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • USDT担保残高とは別です。プール設定無効時は422。
  • V-REC HMAC = learning.token_pool.read / SAKURA Connected Apps = learning_token_payout
GET /api/merchant/v1/token-pool/transfers List pool transfers, statistics and cursor pagination learning.token_pool.read
Auth Merchant HMAC Scope learning.token_pool.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Query parameters

FieldTypeRequiredConstraint / meaning
limitintegerno1–100, default 501–100, default 50
before_idintegernopositive cursor IDpositive cursor ID
statusstringnoreserved|dispatching|submitted|confirmed|reconcile_required|failedreserved|dispatching|submitted|confirmed|reconcile_required|failed

OUT · Response contract

FieldTypePresentConstraint / meaning
dataarrayyes送金台帳の一ページ / One ledger page送金台帳の一ページ / One ledger page
statisticsobjectyes全期間集計(total_count, requested_amount, submitted_count, confirmed_count, reconcile_required_count 等)全期間集計(total_count, requested_amount, submitted_count, confirmed_count, reconcile_required_count 等)
paginationobjectyeslimit, has_more, next_before_idlimit, has_more, next_before_id

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 件数・数量の全期間集計はstatistics。画面1ページの合計ではありません。
  • V-REC HMAC = learning.token_pool.read / SAKURA Connected Apps = learning_token_payout
GET /api/merchant/v1/token-pool/transfers/{id} Read a submitted transfer by request ID learning.token_pool.read
Auth Merchant HMAC Scope learning.token_pool.read Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Path parameters

FieldTypeRequiredConstraint / meaning
idstringyesrequest_id 8–120 [A-Za-z0-9._:-]request_id 8–120 [A-Za-z0-9._:-]

OUT · Response contract

FieldTypePresentConstraint / meaning
request_idstringyes加盟店送金リクエストID / Merchant idempotency identifier加盟店送金リクエストID / Merchant idempotency identifier
statusstringyesreserved|dispatching|submitted|confirmed|reconcile_required|failedreserved|dispatching|submitted|confirmed|reconcile_required|failed
tx_hashstring|nullconditional送信受付時に存在することがある / Optional hash送信受付時に存在することがある / Optional hash
amountstringyes数量 / Exact decimal string数量 / Exact decimal string

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 送信受付とオンチェーン確定は異なります。
  • V-REC HMAC = learning.token_pool.read / SAKURA Connected Apps = learning_token_payout
POST /api/merchant/v1/token-pool/transfers Request gasless ERC-20 transfer from dedicated merchant pool learning.token_pool.write
Auth Merchant HMAC Scope learning.token_pool.write Content-Type application/json Success HTTP 200

IN · Request contract

Authentication / common auth

FieldTypeRequiredConstraint / meaning
X-VREC-Merchant-Keyheader stringyesMerchant API credential key IDactive credential key
X-VREC-Timestampheader integer-stringyesUnix seconds; must be within ±300 seconds of server time(int) cast; outside ±300 => merchant_timestamp_invalid
X-VREC-Nonceheader stringyesReplay-protection nonce; cannot be reused for the same credential^[A-Za-z0-9._:-]{8,128}$; stored for 10 minutes
X-VREC-Signatureheader stringyesLowercase hex HMAC-SHA256 of the canonical stringhash_hmac(sha256); compared after lowercasing header

Request body

FieldTypeRequiredConstraint / meaning
merchant_codestringyesV-REC mrc_... matches HMAC merchantV-REC mrc_... matches HMAC merchant
user_idstringyesV-REC usr_... ACTIVE/FULL memberV-REC usr_... ACTIVE/FULL member
request_idstringyesunique 8–120 [A-Za-z0-9._:-]unique 8–120 [A-Za-z0-9._:-]
amountstringyesdecimal string >0, <=18 decimal digits before token precision validationdecimal string >0, <=18 decimal digits before token precision validation

OUT · Response contract

FieldTypePresentConstraint / meaning
request_idstringyes加盟店送金リクエストID / Merchant idempotency identifier加盟店送金リクエストID / Merchant idempotency identifier
statusstringyesreserved|dispatching|submitted|confirmed|reconcile_required|failedreserved|dispatching|submitted|confirmed|reconcile_required|failed
tx_hashstring|nullconditional送信受付時に存在することがある / Optional hash送信受付時に存在することがある / Optional hash
amountstringyes数量 / Exact decimal string数量 / Exact decimal string

Expected errors / HTTP responses

HTTP 401merchant_auth_requiredFormat: JSON

HMAC認証に失敗 / HMAC authentication failed

HTTP 403merchant_scope_deniedFormat: JSON

HMAC scope、所属、SAKURA Connected Appsが不一致 / insufficient grants or merchant mismatch

HTTP 409merchant_learning_shared_instance_not_isolatedFormat: JSON

共有インスタンスは加盟店分離未保証 / shared instance is not isolated

HTTP 503learning_upstream_unavailableFormat: JSON

SAKURA通信障害。送金の自動再試行は禁止 / Upstream unavailable; do not blindly retry money movement

HTTP 422validation_errorFormat: JSON

Invalid fields, eligibility, balance or wallet

API errors are JSON by default {"error":"<machine_code>"} except responses explicitly marked non-JSON / redirect (including the current end-user session 302 and CSRF 419 behavior).

Implementation notes

  • 運営設定済み原資・署名リレイヤー・受取ウォレット必須。202やtx_hashはブロック確定ではありません。再送は同じrequest_idを使用し、要照合時の再送金は禁止。
  • V-REC HMAC = learning.token_pool.write / SAKURA Connected Apps = learning_token_payout

Webhook lifecycle reference: timing, states, registration, tenant funding and 3DS →

WEBHOOK

Merchant Webhook · user.kyc.updated

V-REC → Merchant
POSTMerchant configured HTTPS URLuser.kyc.updated⌄

IN · Headers received by Merchant

Content-Type: application/json
X-VREC-Event: user.kyc.updated
X-VREC-Delivery: mwd_...
X-VREC-Timestamp: <unix seconds>
X-VREC-Signature: <lowercase hex hmac sha256>

Signature verification string

TIMESTAMP + "\n" + DELIVERY + "\n" + EVENT + "\n" + SHA256(raw_body)

OUT · Merchant response

Any 2xx is success. Failed deliveries make at most 8 attempts with delays of 60, 300, 1800, 7200, 21600, 43200 and 86400 seconds. De-duplicate transport by X-VREC-Delivery and business events by payload id.

Expected: HTTP 200–299

Payload fieldTypeRequiredMeaning
idstringyesStable business event ID across retrieskyc_ + sha256(membership_id|status|source_event_id); stable across retries
typestringyesEvent typeuser.kyc.updated
occurred_atstringyesOccurred atISO 8601
data.user_idstringyesV-REC user ID
data.beneficiary_idstringyesBeneficiary ID
data.external_user_refstring|nullyesMerchant external user referenceJSON key is always present; value may be null
data.kyc_statusstringyesLatest KYC status re-read from SAKURA
data.ekyc_statusstring|nullyesInformational eKYC stateJSON key is always present; value may be null

Payload example

{
    "id": "kyc_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "type": "user.kyc.updated",
    "occurred_at": "2026-10-05T01:00:00Z",
    "data": {
        "user_id": "usr_demo",
        "beneficiary_id": "VREC-DEMO",
        "external_user_ref": null,
        "kyc_status": "APPROVED",
        "ekyc_status": "COMPLETED"
    }
}

Public API security boundary

Never returned by Merchant API: SAKURA password_setup_url, authenticated-user session tokens, full PAN, CVV, PIN. (Exception: kyc.link.issue returns only a SAKURA short URL.)

Full PAN / expiry / CVV are returned only transiently through the authenticated end-user sensitive endpoint after current-password step-up and CSRF, with no-store handling. eKYC redirect URLs are likewise restricted to the authenticated user session.

Internal APIs (/api/internal/v1/*) and the SAKURA Connected App bridge are outside this public contract.