> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keystoneos.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Submitting Instructions

> How each side submits its own instruction and KeyStone confirms the pair.

Instruction submission is one of two ways to create a settlement: each side submits its own instruction independently, and KeyStone confirms them against each other. The trade itself is agreed upstream, on an execution venue or directly between the counterparties. KeyStone accepts pre-agreed instructions - it never selects a counterparty, prices, nets, or takes custody.

Use this route when each side wants to submit for itself, typically because they sit on different platforms and neither will submit on the other's behalf. Use [direct initiation](/concepts/parties-and-legs#two-ways-a-settlement-is-created) when one platform submits for every party at once.

<Note>
  **This route currently confirms exactly two instructions and creates exactly two parties.** That is a property of instruction submission as it works today, not of settlements in general: a settlement carries up to 50 parties and 50 legs, and the party count for any given settlement is set by its template. A template naming three or more roles, such as the tri-party repo, is created through direct initiation with `POST /v1/settlements`.
</Note>

## How it works

```mermaid theme={null}
%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#1a3a35', 'actorBorder': '#2DD4A8', 'actorTextColor': '#e0f2ef', 'actorLineColor': '#2DD4A8', 'signalColor': '#2DD4A8', 'signalTextColor': '#e0f2ef', 'noteBkgColor': '#162e2a', 'noteTextColor': '#e0f2ef', 'noteBorderColor': '#1AAF8B', 'activationBkgColor': '#1a3a35', 'activationBorderColor': '#2DD4A8', 'labelBoxBkgColor': '#1a3a35', 'labelBoxBorderColor': '#2DD4A8', 'labelTextColor': '#e0f2ef', 'loopTextColor': '#2DD4A8'}}}%%
sequenceDiagram
    participant PA as Platform A (Seller)
    participant KS as KeyStone API
    participant PB as Platform B (Buyer)

    PA->>KS: POST /instructions (seller side)
    KS-->>PA: trade_reference: "KS-abc123"

    Note over PA,PB: Platform A shares trade_reference<br/>with Platform B (out-of-band)

    PB->>KS: POST /instructions (buyer side,<br/>trade_reference: "KS-abc123")
    KS->>KS: Confirm both instructions agree,<br/>create settlement
    KS-->>PB: status: "matched", settlement_id: "..."
```

1. **First party submits**: Platform A sends an instruction without a `trade_reference`. KeyStone generates one (format: `KS-{uuid}`).
2. **Share the reference**: Platform A shares the trade reference with Platform B through their own channels (email, chat, API, etc.).
3. **Second party submits**: Platform B sends an instruction with the same `trade_reference`. KeyStone finds the pending counterpart instruction.
4. **Automatic confirmation**: KeyStone validates that both instructions describe the same trade and creates a settlement with both parties confirmed. (On-chain registration follows once compliance clears.)

## What "matched" means

The `pending_match` and `matched` statuses describe **reference correlation, not trade matching**. The trade itself was matched before KeyStone ever saw it - on an execution venue, through an OTC desk, or directly between the counterparties. What KeyStone does is bring the two halves of that already-matched trade together:

* A pending instruction is **not visible to any other participant**. There is no order book to browse and no way for a third party to discover or take the other side of it.
* An instruction can only ever pair with one that carries the **same trade reference** - a value the counterparties exchanged out-of-band when they agreed the trade. Two instructions that never shared a reference cannot cross, structurally.
* A reference has a **match scope**: one you choose yourself pairs only within your own environment, while cross-platform pairing uses the reference KeyStone generates. See [Match scope](#match-scope).
* KeyStone contributes **no economic terms**. Price and quantity arrive on both instructions; KeyStone checks that they are equal and complementary, and rejects the pairing into `pending_match` if they are not. It never sets, improves, or discovers a price.

`matched` therefore records one fact: both halves of a pre-agreed trade have arrived and agree. There is no order matching, no counterparty discovery, and no price formation anywhere in the flow.

## Match scope

The rule is one line: **a reference you choose yourself matches only within your own environment; cross-platform matching uses the reference KeyStone generates, between environments of the same type.**

| Resting instruction                                                             | Counterpart in the same environment | Counterpart in another environment of the same type                                                              | Counterpart in an environment of another type |
| ------------------------------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| KeyStone-generated reference (`KS-...`, submitted with `trade_reference: null`) | Pairs on the reference              | Pairs on the reference - the generated value is unguessable, so presenting it proves the resting party shared it | Never pairs                                   |
| Caller-chosen reference (your own trade id)                                     | Pairs on the reference              | Never pairs                                                                                                      | Never pairs                                   |

Caller-chosen references exist so you can carry your own trade ids - the id from your execution venue, OTC desk, or internal booking system - when both sides of the trade are submitted under your environment. They are ordinary strings a third party could know or guess, so they never reach across environments; for a trade settling between two platforms, always let KeyStone generate the reference and share that.

Environment types (`development`, `staging`, `production`) are isolation worlds. An instruction submitted from a development environment is a test trade and can never become the counterparty of a production instruction, whatever reference it presents - so an integration test cannot consume, or settle against, a live trade.

Both environments must also be active. Deactivating an environment takes its resting instructions out of match scope once the deactivation commits: no credential resolves into a deactivated environment, so its owner could not see the settlement a match would create, act on it, or cancel the instruction behind it. Instructions resting there are left as they are and expire on their own. A match already in flight when the deactivation commits can still land; the settlement it creates is in flight like any other and completes.

A submission that names a reference resting in another environment - in an environment of another type, or in a deactivated one - is not rejected: it is stored as `pending_match` in your own environment, exactly as if you were the first party - and within everything your credential can see, you are. The API never reveals whether a reference is in use in an environment outside your match scope.

When more than one counterparty instruction rests on a reference within your match scope, your submission pairs with the oldest one; if that instruction does not agree, your submission rests as `pending_match` rather than trying a younger one.

## Step 1: Submit the first instruction

The first party submits their side of the trade. Leave `trade_reference` null to have KeyStone generate one.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.keystoneos.xyz/v1/instructions \
    -H "Authorization: Bearer $PLATFORM_A_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "idempotency_key": "seller-instruction-001",
      "template_slug": "dvp-standard",
      "role": "seller",
      "party": {
        "external_reference": "seller-acct-001",
        "name": "Acme Securities",
        "wallet_address": "0x1234567890abcdef1234567890abcdef12345678"
      },
      "legs": [
        {
          "leg_type": "asset_delivery",
          "instrument_id": "0xTokenContractAddress",
          "quantity": "1000",
          "direction": "deliver",
          "chain_id": 11155111
        },
        {
          "leg_type": "payment",
          "instrument_id": "0xUSDCAddress",
          "quantity": "250000",
          "direction": "receive",
          "chain_id": 11155111
        }
      ],
      "timeout_at": "2026-03-20T00:00:00Z"
    }'
  ```

  ```typescript TypeScript SDK theme={null}
  const instruction = await client.instructions.submit({
    idempotencyKey: client.generateIdempotencyKey(),
    templateSlug: "dvp-standard",
    role: "seller",
    party: {
      externalReference: "seller-acct-001",
      name: "Acme Securities",
      walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
    },
    legs: [
      { legType: "asset_delivery", instrumentId: "0xTokenContractAddress", quantity: "1000", direction: "deliver", chainId: 11155111 },
      { legType: "payment", instrumentId: "0xUSDCAddress", quantity: "250000", direction: "receive", chainId: 11155111 },
    ],
    timeoutAt: "2026-03-20T00:00:00Z",
  });

  const tradeReference = instruction.tradeReference; // "KS-abc123..."
  ```

  ```python Python SDK theme={null}
  from keystoneos import KeystoneClient

  async with KeystoneClient(
      client_id="YOUR_CLIENT_ID",
      client_secret="YOUR_CLIENT_SECRET",
  ) as client:
      instruction = await client.instructions.submit({
          "idempotency_key": "seller-instruction-001",
          "template_slug": "dvp-standard",
          "role": "seller",
          "party": {
              "external_reference": "seller-acct-001",
              "name": "Acme Securities",
              "wallet_address": "0x1234567890abcdef1234567890abcdef12345678",
          },
          "legs": [
              {"leg_type": "asset_delivery", "instrument_id": "0xTokenContractAddress", "quantity": "1000", "direction": "deliver", "chain_id": 11155111},
              {"leg_type": "payment", "instrument_id": "0xUSDCAddress", "quantity": "250000", "direction": "receive", "chain_id": 11155111},
          ],
          "timeout_at": "2026-03-20T00:00:00Z",
      })

      trade_reference = instruction.trade_reference  # "KS-abc123..."
  ```
</CodeGroup>

Response:

```json theme={null}
{
  "id": "inst-...",
  "trade_reference": "KS-abc123-def4-5678-...",
  "status": "pending_match",
  "role": "seller",
  "settlement_id": null,
  "created_at": "2026-03-19T10:00:00Z"
}
```

You may submit your own `trade_reference` instead - for example the trade id from your execution venue - but it pairs only with instructions in your own environment. For a trade settling with another platform, leave the field null and use the generated reference (see [Match scope](#match-scope)).

## Step 2: Share the trade reference

The trade reference (`KS-abc123...`) is shared between counterparties through their own communication channels. KeyStone does not handle this exchange - it is an out-of-band coordination step. Treat a generated reference like a secret: anyone you give it to can submit the other side of the trade.

Common methods:

* Platform-to-platform API integration
* Pre-agreed trade reference via OTC desk
* Shared trade confirmation system

## Step 3: Submit the counterpart instruction

The second party submits their instruction with the trade reference from Step 1.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.keystoneos.xyz/v1/instructions \
    -H "Authorization: Bearer $PLATFORM_B_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "idempotency_key": "buyer-instruction-001",
      "trade_reference": "KS-abc123-def4-5678-...",
      "template_slug": "dvp-standard",
      "role": "buyer",
      "party": {
        "external_reference": "buyer-acct-042",
        "name": "Beta Capital",
        "wallet_address": "0xfedcba9876543210fedcba9876543210fedcba98"
      },
      "legs": [
        {
          "leg_type": "asset_delivery",
          "instrument_id": "0xTokenContractAddress",
          "quantity": "1000",
          "direction": "receive",
          "chain_id": 11155111
        },
        {
          "leg_type": "payment",
          "instrument_id": "0xUSDCAddress",
          "quantity": "250000",
          "direction": "deliver",
          "chain_id": 11155111
        }
      ],
      "timeout_at": "2026-03-20T00:00:00Z"
    }'
  ```

  ```typescript TypeScript SDK theme={null}
  const buyerInstruction = await platformBClient.instructions.submit({
    idempotencyKey: platformBClient.generateIdempotencyKey(),
    tradeReference: "KS-abc123-def4-5678-...",
    templateSlug: "dvp-standard",
    role: "buyer",
    party: {
      externalReference: "buyer-acct-042",
      name: "Beta Capital",
      walletAddress: "0xfedcba9876543210fedcba9876543210fedcba98",
    },
    legs: [
      { legType: "asset_delivery", instrumentId: "0xTokenContractAddress", quantity: "1000", direction: "receive", chainId: 11155111 },
      { legType: "payment", instrumentId: "0xUSDCAddress", quantity: "250000", direction: "deliver", chainId: 11155111 },
    ],
    timeoutAt: "2026-03-20T00:00:00Z",
  });

  console.log(buyerInstruction.status);       // "matched"
  console.log(buyerInstruction.settlementId); // "550e8400-..."
  ```

  ```python Python SDK theme={null}
  buyer_instruction = await platform_b_client.instructions.submit({
      "idempotency_key": "buyer-instruction-001",
      "trade_reference": "KS-abc123-def4-5678-...",
      "template_slug": "dvp-standard",
      "role": "buyer",
      "party": {
          "external_reference": "buyer-acct-042",
          "name": "Beta Capital",
          "wallet_address": "0xfedcba9876543210fedcba9876543210fedcba98",
      },
      "legs": [
          {"leg_type": "asset_delivery", "instrument_id": "0xTokenContractAddress", "quantity": "1000", "direction": "receive", "chain_id": 11155111},
          {"leg_type": "payment", "instrument_id": "0xUSDCAddress", "quantity": "250000", "direction": "deliver", "chain_id": 11155111},
      ],
      "timeout_at": "2026-03-20T00:00:00Z",
  })

  print(buyer_instruction.status)         # "matched"
  print(buyer_instruction.settlement_id)  # "550e8400-..."
  ```
</CodeGroup>

If the instructions agree, the response includes the settlement:

```json theme={null}
{
  "id": "inst-...",
  "trade_reference": "KS-abc123-def4-5678-...",
  "status": "matched",
  "settlement_id": "550e8400-...",
  "created_at": "2026-03-19T10:05:00Z"
}
```

### Refused: an instruction with this role already rests

A submission whose role already has a resting (`pending_match`, unexpired) instruction on the same trade reference within your match scope is refused with `409 DUPLICATE_RESOURCE`, and nothing is written. This is what a retry with a fresh `idempotency_key` after a timed-out first submission looks like: the first submission landed. A retry that reuses the original `idempotency_key` returns that instruction instead. To replace a resting instruction, cancel it first (`DELETE /v1/instructions/{id}`; list your pending instructions with `GET /v1/instructions?status=pending_match` to find it) or wait for it to expire. The refusal reveals only that a same-role instruction rests within your match scope, nothing about where: for a Keystone-generated reference, that instruction can be in another environment. Two same-role submissions that race each other can both pass this check and both rest; each counterparty submission then pairs with one of them, and the second pairing creates a second settlement with its own deposit keys that reaches its deadline like any other settlement and that no endpoint cancels. It announces itself the way any settlement does: a `settlement_id` on the `201` that created it, then its own `settlement.state.awaiting_deposits`. Where a duplicate submission is possible, reconcile each `settlement_id` against the trade you intended before funding, rather than funding every deposit window you are told about.

## What confirmation validates

Confirmation only considers a resting instruction that is within the submission's match scope (see [Match scope](#match-scope)) and not past its `expires_at`. A lapsed instruction is skipped even before the expiry sweep marks it `expired`, so a counterpart can be out of reach while it still reads `pending_match` - see [Expiry](#expiry). Both instructions must then agree on:

| Field           | Validation                                                                                                                                                                                                                                                                          |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `template_slug` | Must be identical                                                                                                                                                                                                                                                                   |
| `role`          | Must be different (one seller, one buyer)                                                                                                                                                                                                                                           |
| Legs            | Must pair one-to-one: every leg on each side needs exactly one complementary leg on the other - same `instrument_id`, equal quantity, opposite direction (seller delivers what buyer receives). An extra, duplicate, or otherwise unmatched leg on either side refuses the pairing. |

Both sides' stored data must also be admissible: before the settlement is created, each instruction is re-checked against what submission itself admits, and the match is refused when, among other checks, a stored `wallet_address` is outside the required form, a party `chain_id` is outside the stored range, a party field exceeds its length bound, or a leg's `leg_type`, `token_standard`, or `chain_id` is outside the type and length bounds submission enforces (see the field rules in [parties and legs](/concepts/parties-and-legs)). An instruction that submission accepts as it now stands always passes this check, so only a resting instruction whose stored data submission would no longer admit can refuse the match; it stays unmatched until it expires or is cancelled.

These are not the only conditions a match needs, but every check in this section refuses a pairing the same way: the second instruction is still saved as `pending_match`. It will not be automatically paired with the first instruction. A `role` collision never gets this far: a submission whose role already rests on the reference within your match scope is refused at intake with `409 DUPLICATE_RESOURCE` and nothing is written (see [Refused: an instruction with this role already rests](#refused-an-instruction-with-this-role-already-rests)). For a `template_slug` or legs refusal, the submitting platform can cancel it and resubmit with corrected details. When the refusal came from the resting counterpart instead - it has lapsed, or its stored data is what submission would no longer admit - that remedy reproduces the refusal: the instruction that must change is the resting one, and only the platform that submitted it can cancel it. While it still reads `pending_match` it can be cancelled and resubmitted with corrected details; a lapsed one can no longer match whether or not it is cancelled, so a fresh instruction for that side is what completes the trade. The second instruction itself needs no resubmission: it stays eligible until its own `expires_at`.

## Optional fields

| Field        | Description                                                                                                                                                                                          |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trade_type` | Set to `repo_open` for repo opening legs. See [Tokenised Repo](/guides/tokenised-repo).                                                                                                              |
| `repo_terms` | Required for `repo_open` - the repo economics (`tenor_days`, `rate_bps`, `haircut_bps`, `margin_band_lower`, `margin_band_upper`). Maturity and the closing repayment amount are derived from these. |

## Cancellation

Cancel a pending instruction if it was submitted in error or the trade is no longer needed:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE https://api.keystoneos.xyz/v1/instructions/$INSTRUCTION_ID \
    -H "Authorization: Bearer $TOKEN"
  ```

  ```typescript TypeScript SDK theme={null}
  await client.instructions.cancel("inst-...");
  ```

  ```python Python SDK theme={null}
  await client.instructions.cancel("inst-...")
  ```
</CodeGroup>

Only instructions with `status: pending_match` can be cancelled. Instructions already in `matched` or `expired` status cannot be cancelled.

## Expiry

Instructions expire 24 hours after submission; the deadline is returned on the instruction as `expires_at`. Eligibility for confirmation ends at `expires_at` itself, not when the periodic sweep later stamps the row `expired`: until the sweep runs, a lapsed instruction still reads `pending_match` but can no longer match. This prevents stale instructions from accidentally pairing with new ones submitted days later.

## Listing instructions

View your platform's instructions:

<CodeGroup>
  ```bash cURL theme={null}
  # All instructions
  curl https://api.keystoneos.xyz/v1/instructions \
    -H "Authorization: Bearer $TOKEN"

  # Filter by status
  curl "https://api.keystoneos.xyz/v1/instructions?status=pending_match" \
    -H "Authorization: Bearer $TOKEN"
  ```

  ```typescript TypeScript SDK theme={null}
  // All instructions
  const { items } = await client.instructions.list();

  // Filter by status
  const pending = await client.instructions.list({ status: "pending_match" });
  ```

  ```python Python SDK theme={null}
  # All instructions
  result = await client.instructions.list()

  # Filter by status
  pending = await client.instructions.list(status="pending_match")
  ```
</CodeGroup>

## Single-platform use

Paired instructions work for single-platform settlements too. When both instructions come from the same platform, the settlement is created as `single_platform` type. This is useful when your platform intermediates trades between its own users.

<CardGroup cols={2}>
  <Card title="Escrow Deposits" icon="lock" href="/guides/escrow-deposits">
    How parties deposit into the settlement contract with commitment secrets.
  </Card>

  <Card title="Escrow Deposits" icon="lock" href="/guides/escrow-deposits">
    How parties deposit to escrow using commitment secrets.
  </Card>
</CardGroup>
