Inyo

Integration Guide

This guide walks through a real B2B scenario end-to-end.

Scenario: ACME Corp wires $10,000 USD to Inyo. That money is split across 10 sub-wallets β€” one per vendor β€” with $1,000 each. Vendor 03 later runs a $500 preauthorization for an outstanding invoice, captures $400 of it (the invoice came in lower than expected), refunds $50 to the customer, and finally voids the remaining hold.

Every dollar remains traceable back to the original wire.

For the examples below, export these once:

export WALLET_HOST=https://{FQDN}
export TENANT=acme-corp
export API_KEY=your-tenant-api-key

Step 1 β€” Create the Wallets

You need 11 wallets: one "treasury" wallet that receives the wire, and 10 vendor wallets that receive the split.

curl -sS -X POST "$WALLET_HOST/organizations/$TENANT/wallets" \
  -H "X-API-KEY: $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: acme-treasury-2026-07-14" \
  -d '{
    "owner_type": "company",
    "owner_ref": "acme-corp",
    "name": "ACME Treasury"
  }'

Response:

{
  "id": "f64c49bd-47a3-4cc6-b9d4-265d1fe5ba65",
  "name": "ACME Treasury",
  "kind": "operational",
  "ownerType": "company",
  "ownerRef": "acme-corp",
  "createdAt": "2026-07-14T04:01:33+00:00"
}

Repeat for the 10 vendor wallets with different owner_ref and name values, and store the returned UUIDs. Below, $VENDOR_03_ID refers to vendor 03's wallet.

Naming your Idempotency-Key: use something meaningful and unique per operation (e.g., acme-vendor-03-2026-07-14). Reusing the same key returns the original response instead of creating a duplicate β€” safe to retry on network failure.


Step 2 β€” Deposit + Split the $10k Wire

ACME's bank confirmed the wire hit your settlement account. Record it in the ledger and split it across the 10 vendors in one atomic operation:

curl -sS -X POST "$WALLET_HOST/organizations/$TENANT/deposits" \
  -H "X-API-KEY: $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wire-BANKREF-20260714-001" \
  -d '{
    "source": {
      "source_type": "wire",
      "external_ref": "BANK-WIRE-98765432",
      "asset_code": "USD",
      "amount": "10000.00",
      "received_at": "2026-07-14T14:30:00Z",
      "metadata": {
        "originator": "ACME Corp",
        "reference": "Q3 vendor payments"
      }
    },
    "splits": [
      {"wallet_id": "<VENDOR_01_ID>", "amount": "1000.00"},
      {"wallet_id": "<VENDOR_02_ID>", "amount": "1000.00"},
      {"wallet_id": "<VENDOR_03_ID>", "amount": "1000.00"},
      {"wallet_id": "<VENDOR_04_ID>", "amount": "1000.00"},
      {"wallet_id": "<VENDOR_05_ID>", "amount": "1000.00"},
      {"wallet_id": "<VENDOR_06_ID>", "amount": "1000.00"},
      {"wallet_id": "<VENDOR_07_ID>", "amount": "1000.00"},
      {"wallet_id": "<VENDOR_08_ID>", "amount": "1000.00"},
      {"wallet_id": "<VENDOR_09_ID>", "amount": "1000.00"},
      {"wallet_id": "<VENDOR_10_ID>", "amount": "1000.00"}
    ]
  }'

Response (abbreviated β€” the full response has all 11 entries):

{
  "id": "419b03c7-7679-4bce-b3d9-d90f75418602",
  "kind": "deposit",
  "externalRef": "wire-BANKREF-20260714-001",
  "metadata": {
    "source_uuid": "268eda6f-ed57-4e8c-9a07-1b39f15ec817"
  },
  "entries": [
    { "assetCode": "USD", "direction": "debit",  "amount": "10000.00000000", "lineageRef": null },
    { "assetCode": "USD", "direction": "credit", "amount": "1000.00000000",  "lineageRef": [7] }
  ],
  "createdAt": "2026-07-14T14:30:12+00:00"
}

What just happened:

  1. A source row was created recording the wire.
  2. A journal of kind deposit was written with 11 entries: one debit of $10,000 against the external-source account ("money coming from outside the ledger") and 10 credits of $1,000 against the vendor wallets.
  3. Every credit entry carries a lineageRef pointing at the source.
  4. Sum of debits equals sum of credits β€” the double-entry invariant holds.

The source.amount must equal the sum of splits β€” otherwise the API returns a LEDGER_IMBALANCE error.

To keep a portion of the wire in a system suspense account (settlement fees, ops float), pass gateway_suspense_amount at the top level. Then sum(splits) + gateway_suspense_amount must equal source.amount.

Accepted source_type values: wire, ach_debit, card, stablecoin_deposit.


Step 3 β€” Check Balances

curl -sS "$WALLET_HOST/organizations/$TENANT/wallets/$VENDOR_03_ID/balance" \
  -H "X-API-KEY: $API_KEY"
[
  {
    "assetCode": "USD",
    "posted": "1000.00000000",
    "held": "0.00000000",
    "available": "1000.00000000",
    "updatedAt": "2026-07-14T14:30:12+00:00"
  }
]

Vendor 03 has $1,000 available. If a wallet holds multiple assets, filter with ?asset=USD β€” this returns a single-element array (or empty if no balance in that asset).


Step 4 β€” Preauthorize $500

Vendor 03 has an incoming invoice. Reserve $500 before you know the exact amount so the money can't be spent elsewhere:

curl -sS -X POST "$WALLET_HOST/organizations/$TENANT/holds" \
  -H "X-API-KEY: $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: preauth-invoice-INV-4521" \
  -d "{
    \"wallet_id\": \"$VENDOR_03_ID\",
    \"asset_code\": \"USD\",
    \"amount\": \"500.00\",
    \"expires_at\": \"2026-07-21T14:30:00Z\"
  }"

Response:

{
  "id": "19f60a03-7a09-4184-bf66-23ce30115377",
  "assetCode": "USD",
  "amount": "500.00",
  "status": "open",
  "expiresAt": "2026-07-21T14:30:00Z",
  "resolvedAt": null,
  "createdAt": "2026-07-14T15:00:00+00:00",
  "captures": []
}

Save the hold id β€” you'll capture/void against it. The balance now shows:

[
  { "assetCode": "USD", "posted": "500.00000000", "held": "500.00000000", "available": "0.00000000" }
]
  • posted dropped from $1,000 to $500 β€” the preauth debited the wallet and credited a per-wallet hold sub-account.
  • held shows the $500 reservation.
  • available is $0 β€” every remaining dollar is reserved.

expires_at is advisory. The hold records the expiration, but nothing auto-voids expired holds. Track hold IDs and expirations on your side and void explicitly when they lapse.


Step 5 β€” Capture $400 of the $500 Hold

The invoice came in at $400. Capture that amount into a destination account β€” typically a payables/settlement wallet you created beforehand:

curl -sS -X POST "$WALLET_HOST/organizations/$TENANT/holds/$HOLD_ID/capture" \
  -H "X-API-KEY: $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: capture-INV-4521" \
  -d "{
    \"amount\": \"400.00\",
    \"destination_account_id\": \"$PAYABLES_WALLET_ID\"
  }"

Response:

{
  "id": "b092a884-8cc4-479d-80b5-995e668a8cdb",
  "kind": "capture",
  "metadata": {
    "hold_uuid": "19f60a03-7a09-4184-bf66-23ce30115377",
    "captured_amount": "400.00"
  },
  "entries": [
    { "assetCode": "USD", "direction": "debit",  "amount": "400.00000000", "lineageRef": [7] },
    { "assetCode": "USD", "direction": "credit", "amount": "400.00000000", "lineageRef": [7] }
  ]
}

Save the journal id (b092a884-...) β€” this is the capture journal UUID, required to refund.

Balance after capture: posted 500 / held 100 / available 400. The $400 was released from the hold and moved to the destination; the $100 that was reserved above the invoice amount stays held.

Partial captures: you can capture multiple times against the same hold until the cumulative amount equals the hold amount. Status transitions open β†’ partially_captured β†’ captured. Once fully captured, no further captures or voids are possible β€” only refunds.


Step 6 β€” Refund $50 of the $400 Capture

The customer disputed part of the invoice. Refund uses the capture journal UUID from step 5, not the hold ID:

curl -sS -X POST "$WALLET_HOST/organizations/$TENANT/captures/$CAPTURE_JOURNAL_UUID/refund" \
  -H "X-API-KEY: $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund-INV-4521-partial" \
  -d '{ "amount": "50.00" }'

The refund debits the destination (payables wallet) and credits back to the originating wallet (Vendor 03) β€” the ledger looks up the original hold to find the source wallet. Lineage is preserved through the reversal chain.

Balance: posted 550 / held 100 / available 450.

You can refund partially multiple times until the cumulative refunded amount equals the captured amount. A fully-refunded capture rejects further refunds.


Step 7 β€” Void the Remaining $100 Hold

The invoice is settled ($400 captured, $50 refunded). Release the remaining $100:

curl -sS -X POST "$WALLET_HOST/organizations/$TENANT/holds/$HOLD_ID/void" \
  -H "X-API-KEY: $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: void-INV-4521-remainder" \
  -d '{}'

Balance: posted 650 / held 0 / available 650.

Vendor 03's final position: $1,000 in βˆ’ $400 captured + $50 refunded = $650. Every dollar traceable back to the original wire.


Step 8 β€” Transfer Between Wallets

Move $200 from Vendor 03 to Vendor 07 (same currency):

curl -sS -X POST "$WALLET_HOST/organizations/$TENANT/transfers" \
  -H "X-API-KEY: $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: transfer-v03-to-v07-20260714" \
  -d "{
    \"from_wallet_id\": \"$VENDOR_03_ID\",
    \"to_wallet_id\": \"$VENDOR_07_ID\",
    \"asset_code\": \"USD\",
    \"amount\": \"200.00\"
  }"

The response is a kind: transfer journal with two entries. Transfers are same-currency only β€” for cross-asset conversions, use POST /fx-conversions with explicit rate and rate_source fields (both are echoed back in the journal's metadata for audit).


Reading the Ledger

List every debit and credit on a wallet, most recent first (paginated, 50 per page; filter by asset, journal_kind, and date range):

curl -sS "$WALLET_HOST/organizations/$TENANT/wallets/$VENDOR_03_ID/entries?asset=USD" \
  -H "X-API-KEY: $API_KEY"

Each entry includes lineageRef β€” the source IDs that funded that specific movement β€” letting you trace any debit backwards to prove where every dollar came from.

Fetch a hold with its capture history:

curl -sS "$WALLET_HOST/organizations/$TENANT/holds/$HOLD_ID" \
  -H "X-API-KEY: $API_KEY"

There is no list endpoint for holds β€” store hold IDs (and their expirations) in your own system as you create them.

See the API Reference for the full endpoint table, error codes, and idempotency details.