Counterparty (LP) API Reference

For liquidity providers integrating with the Interstice cross-chain swap engine (Canton Network ⇄ Ethereum).

This page is reference material for whoever authors and maintains the published partner integration docs — use it as source content when writing those pages, not as something to hand to a partner directly. It states the current interface as-is and intentionally doesn't call out what changed from any earlier version; see the separate change log for that.

This API lets a counterparty (liquidity provider) discover swap requests, quote on them, and settle the resulting swaps — locking and claiming funds on both Canton and Ethereum as part of a hash-timelocked atomic swap. All partner-facing routes live under one versioned path, /external/cp/v1, and are reached through a dedicated gateway rather than any internal Interstice service.

Authentication

Every request is authenticated with a bearer API key issued to your organization during onboarding:

Authorization: Bearer isk_<your-key>

Keys are entity-scoped — a key only ever acts on behalf of the counterparty it was issued to, and can be rotated or revoked at any time from your account. There is no separate signing step for standard API calls; signing only applies to the on-chain legs of settlement (see Settlement flow).

Base URL & environments

Your base URL and environment (testnet or production) are provided as part of onboarding, along with the Canton package id and Ethereum contract address you'll transact against — both of which are environment-specific and allowlisted per partner. All endpoints below are shown as paths relative to that base URL.

All amounts are whole-unit decimal strings (e.g. "1.5" ETH — never wei/base units, never a JSON number). All timestamps are ISO-8601 strings. All ids are opaque strings. Errors use a consistent envelope:

{
  "error": {
    "code": "rfq.not_eligible",
    "message": "This counterparty is not eligible to quote on this RFQ.",
    "details": {}
  }
}

Discovery & profile

GET/config

Everything needed to self-configure — chain ids, HTLC addresses, Canton package/synchronizer/scan, assets, platform limits.

Response — 200
FieldType
networks[].chainIdinteger
networks[].namestring
networks[].htlcAddressstring (0x address)
canton.synchronizerIdstring
canton.packageIdstring
canton.scanUrlstring (url)
assets[].symbolstring
assets[].chain"ethereum" | "canton"
assets[].decimalsinteger
platform.minSwapUsd / maxSwapUsdnumber
platform.maxFeeBps / maxSlippageBpsinteger
{
  "networks": [
    { "chainId": 11155111, "name": "sepolia", "htlcAddress": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b" }
  ],
  "canton": {
    "synchronizerId": "global-domain::1220a1b2c3d4e5f6a7b8c9d0e1f2a3b4",
    "packageId": "49fcb32710064fd77d7a3d72d19d4360a79b55d1123e2bd035e9ccf81a3a8108",
    "scanUrl": "https://scan.example.canton.network"
  },
  "assets": [
    { "symbol": "ETH", "chain": "ethereum", "decimals": 18 },
    { "symbol": "CC",  "chain": "canton",   "decimals": 10 }
  ],
  "platform": { "minSwapUsd": 100, "maxSwapUsd": 250000, "maxFeeBps": 50, "maxSlippageBps": 100 }
}
GET/me

Your counterparty profile.

Response — 200
FieldType
entityIdstring
namestring
statusstring
cantonOnboardedboolean
supportedPairsstring[]
{
  "entityId": "cp_9f2a1b3c",
  "name": "Example Liquidity LLC",
  "status": "ACTIVE",
  "cantonOnboarded": true,
  "supportedPairs": ["ETH/CC", "USDC/CC"]
}
PATCH/me/supported-pairs

Set the pairs you're eligible to quote on.

Request body
FieldType
pairsstring[]
{ "pairs": ["ETH/CC", "USDC/CC"] }
Response — 200

Updated CpProfile — same shape as GET /me above.

GET/me/fee

Your effective fee tier.

Response — 200
FieldType
tierstring
platformFeeBpsinteger
{ "tier": "partner", "platformFeeBps": 10 }
GET/me/balances

Your own Canton CC balance.

Response — 200
FieldType
canton.ccdecimal string
canton.unlockeddecimal string
{ "canton": { "cc": "15230.75", "unlocked": "12000.00" } }

RFQ & quoting

GET/rfqs

Open requests you're eligible to quote (privacy-stripped — no requester identity unless anonymized is false).

Query params
FieldTypeRequired
cursorstringno
limitintegerno
pairstringno
directionstringno
Response — 200
FieldType
data[].idstring
data[].direction"ONRAMP" | "OFFRAMP"
data[].pairstring, "<evmAsset>/<cantonAsset>"
data[].origination{ network, asset, amount }
data[].destination{ network, asset }
data[].maxSlippageBpsinteger
data[].anonymizedboolean
data[].requesterIdstring or null
data[].createdAt / expiresAtISO-8601 string
cursorstring or null
hasMoreboolean
{
  "data": [
    {
      "id": "rfq_3d8f0a91",
      "direction": "ONRAMP",
      "pair": "ETH/CC",
      "origination": { "network": "ethereum", "asset": "ETH", "amount": "1.5" },
      "destination": { "network": "canton", "asset": "CC" },
      "maxSlippageBps": 50,
      "anonymized": true,
      "requesterId": null,
      "createdAt": "2026-07-02T14:00:00Z",
      "expiresAt": "2026-07-02T14:05:00Z"
    }
  ],
  "cursor": null,
  "hasMore": false
}
GET/rfqs/:id

Detail on a specific request (404 if not found or not eligible).

Path params
FieldType
idstring — RFQ id
Response — 200

A single CpRfqSummary object — same shape as one item in data[] above.

POST/rfqs/:id/quote

Submit an offered rate.

Path params
FieldType
idstring — RFQ id
Request body
FieldTypeNotes
offeredRatedecimal stringdestination per origination unit
destinationAmountdecimal stringload-bearing settlement figure
originationReceiverstringadvisory — server resolves the signing party
destinationSenderstring
validUntilISO-8601 string
{
  "offeredRate": "2665.40",
  "destinationAmount": "3998.10",
  "originationReceiver": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
  "destinationSender": "cp-lp-party::1220a1b2c3d4e5f6",
  "validUntil": "2026-07-02T14:05:00Z"
}
Response — 201
FieldType
quoteIdstring
rfqIdstring
stateACTIVE | ACCEPTED | REJECTED | EXPIRED | WITHDRAWN
offeredRate / destinationAmountdecimal string
createdAt / validUntilISO-8601 string
{
  "quoteId": "quote_5b7e2c14",
  "rfqId": "rfq_3d8f0a91",
  "state": "ACTIVE",
  "offeredRate": "2665.40",
  "destinationAmount": "3998.10",
  "createdAt": "2026-07-02T14:00:12Z",
  "validUntil": "2026-07-02T14:05:00Z"
}

Errors: 404 rfq.not_found / rfq.not_eligible, 409 rfq.duplicate_quote, 422 quote guard failed.

DELETE/rfqs/:id/quote/:quoteId

Withdraw a live quote before it's accepted.

Path params
FieldType
idstring — RFQ id
quoteIdstring — quote id
Response — 204

No body.

GET/quotes

Your outstanding quotes across all open requests.

Query params
FieldType
cursorstring
limitinteger
statestring
Response — 200

Paginated list of CpQuote — same item shape as the response of POST /rfqs/:id/quote above, wrapped in { data, cursor, hasMore }.

Swaps

GET/swaps

Your swaps.

Query params
FieldType
cursorstring
limitinteger
statestring
Response — 200
FieldType
data[].idstring
data[].direction"ONRAMP" | "OFFRAMP"
data[].pairstring
data[].statesee swap states below
data[].hashlockstring (0x, 32-byte hex)
data[].role"maker"
data[].createdAt / updatedAtISO-8601 string

Swap states: HASHLOCK_SHARED, CANTON_LOCKED, ETH_LOCKED, ETH_CLAIMED, CANTON_CLAIMED, COMPLETED, REFUNDING, REFUNDED, REORG_DETECTED.

{
  "data": [
    {
      "id": "swap_7e4c1a2b",
      "direction": "ONRAMP",
      "pair": "ETH/CC",
      "state": "ETH_LOCKED",
      "hashlock": "0x1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d",
      "role": "maker",
      "createdAt": "2026-07-02T14:00:20Z",
      "updatedAt": "2026-07-02T14:02:05Z"
    }
  ],
  "cursor": null,
  "hasMore": false
}
GET/swaps/:id

Full swap detail. This shape is reused as the response body of every report-* call below.

Path params
FieldType
idstring — swap id
Response — 200
FieldType
everything in the swap summary above, plus:
origination / destination{ network, asset, amount }
timelocks.ethExpiresAt / cantonExpiresAtISO-8601 string
fee{ platformFeeBps, feeChain } or null
legs.ethSwapIdstring, optional
legs.cantonHtlcCidstring, optional
{
  "id": "swap_7e4c1a2b",
  "direction": "ONRAMP",
  "pair": "ETH/CC",
  "state": "ETH_LOCKED",
  "hashlock": "0x1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d",
  "role": "maker",
  "createdAt": "2026-07-02T14:00:20Z",
  "updatedAt": "2026-07-02T14:02:05Z",
  "origination": { "network": "ethereum", "asset": "ETH", "amount": "1.5" },
  "destination": { "network": "canton", "asset": "CC", "amount": "3998.10" },
  "timelocks": { "ethExpiresAt": "2026-07-02T16:00:00Z", "cantonExpiresAt": "2026-07-02T20:00:00Z" },
  "fee": { "platformFeeBps": 10, "feeChain": "ethereum" },
  "legs": { "ethSwapId": "0x9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c" }
}
GET/swaps/:id/confirmation

Trade confirmation for a swap.

Path & query params
FieldType
idpath — string, swap id
formatquery — "json" | "pdf", optional
Response — 200 (format=json)
FieldType
swapId, direction, pair, stateas above
origination / destination{ network, asset, amount }
fee{ platformFeeBps, feeChain } or null
completedAtISO-8601 string or null
legs.ethTxHash / cantonUpdateIdstring, optional
{
  "swapId": "swap_7e4c1a2b",
  "direction": "ONRAMP",
  "pair": "ETH/CC",
  "state": "COMPLETED",
  "origination": { "network": "ethereum", "asset": "ETH", "amount": "1.5" },
  "destination": { "network": "canton", "asset": "CC", "amount": "3998.10" },
  "fee": { "platformFeeBps": 10, "feeChain": "ethereum" },
  "completedAt": "2026-07-02T14:12:40Z",
  "legs": {
    "ethTxHash": "0x3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f",
    "cantonUpdateId": "update_1220a1b2c3d4"
  }
}

Canton signing

Server-prepared, thin-client signing: Interstice builds the unsigned transaction, you sign only the hash, and Interstice never holds your key. You must recompute the prepared-tx hash locally and compare before signing (parity check).

POST/swaps/:id/canton/prepare

Server builds the unsigned Canton transaction.

Path params
FieldType
idstring — swap id
Request body
FieldType
actionenum — see below
paramsobject, optional

action one of: accept-swap-request, prepare-amulet-factory, prepare-amulet-lock, lock-canton, claim-canton, refund-canton, sweep-canton-refund.

{ "action": "lock-canton", "params": { "hashlock": "0x1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d" } }
Response — 200
FieldType
preparedTransactions[].hashbase64 string — recompute and compare before signing
preparedTransactions[].txstring
preparedTransactions[].metaobject, optional
{
  "preparedTransactions": [
    {
      "hash": "cHJlcGFyZWQtdHgtaGFzaC1leGFtcGxl",
      "tx": "base64-or-hex-encoded-unsigned-transaction-payload",
      "meta": { "step": "lock-canton" }
    }
  ]
}
POST/swaps/:id/canton/execute

Submit your signed transaction. Interstice relays it — non-custodial.

Path params
FieldType
idstring — swap id
Request body
FieldType
actionsame enum as canton/prepare
txstring — the prepared transaction
signaturebase64 string — Ed25519
signedBystring — key fingerprint
{
  "action": "lock-canton",
  "tx": "base64-or-hex-encoded-unsigned-transaction-payload",
  "signature": "MEUCIQDx3example9c9SIGNATUREbase64ExampleValueOnly8f9a0b==",
  "signedBy": "fingerprint_9f2a1b3c"
}
Response — 200
FieldType
updateIdstring
contractIdstring, optional
{ "updateId": "update_1220a1b2c3d4", "contractId": "00a1b2c3d4e5f6..." }

On-chain reports

As you complete each step of settlement on-chain, you report it so the swap's verified state machine advances. Interstice independently verifies each report against the relevant chain before accepting it. Every report endpoint below returns 200 with the full swap detail — same shape as GET /swaps/:id above — reflecting the new state.

POST/swaps/by-commitment/:commitment/report-htlc-created

Report your first lock on a quote, before the swap record exists — keyed by quote commitment, not swap id.

Path params
FieldType
commitmentstring — quote commitment (64-hex)
Request body
FieldType
htlcCidstring, optional
{ "htlcCid": "00c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9" }
POST/swaps/:id/report-hashlock

Report the agreed hashlock (ONRAMP sets it / OFFRAMP confirms it).

Request body
FieldType
hashlockstring (0x, 32-byte hex)
{ "hashlock": "0x1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d" }
POST/swaps/:id/report-canton-lock

Report a Canton-side lock.

Request body
FieldType
contractIdstring, optional
txIdstring, optional
lockedAmuletEventBlobstring
lockedAmuletTemplateIdstring, optional
{
  "contractId": "00a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5",
  "lockedAmuletEventBlob": "eventblob-base64-or-hex-example"
}
POST/swaps/:id/report-htlc-refresh

Report an HTLC contract-id rotation (timelock extension before expiry).

Request body
FieldType
newContractIdstring
txIdstring, optional
lockedAmuletEventBlobstring
lockedAmuletTemplateIdstring, optional
{
  "newContractId": "00b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "lockedAmuletEventBlob": "eventblob-base64-or-hex-example"
}
POST/swaps/:id/report-eth-lock

Report an Ethereum-side lock (OFFRAMP).

Request body
FieldType
txHashstring
ethSwapIdstring
feeAmountdecimal string, optional
feeBpsinteger, optional
feeCollectorstring (0x address), optional
{
  "txHash": "0x3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f",
  "ethSwapId": "0x9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c",
  "feeAmount": "0.0015",
  "feeBps": 10,
  "feeCollector": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
}
POST/swaps/:id/report-eth-claim

Report an Ethereum-side claim. The preimage is never sent — it's revealed on-chain by the claim transaction itself.

Request body
FieldType
txHashstring
{ "txHash": "0x84cc9dfb84cc9dfb84cc9dfb84cc9dfb84cc9dfb84cc9dfb84cc9dfb84cc9dfb" }
POST/swaps/:id/report-canton-claim

Report a Canton-side claim — typically the step that moves a swap to COMPLETED.

Request body
FieldType
contractIdstring, optional
updateIdstring, optional
markerMetadataobject, optional
{ "contractId": "00c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7", "updateId": "update_1220a1b2c3d4" }
POST/swaps/:id/report-refund

Report a refund of your own leg after its timelock expires.

Request body
FieldType
chain"ethereum" | "canton"
txHashstring, optional (Ethereum)
contractIdstring, optional (Canton)
{ "chain": "ethereum", "txHash": "0x5e6f7a8b5e6f7a8b5e6f7a8b5e6f7a8b5e6f7a8b5e6f7a8b5e6f7a8b5e6f7a8b" }

Webhooks

Event payloads never contain a preimage. Deliveries are signed: X-Webhook-Signature: sha256=<hmac(rawBody)>.

POST/webhooks

Register an endpoint. The signing secret is shown once, at registration.

Request body
FieldType
callbackUrlstring (url)
eventsstring[] — event types, see below

Event types: rfq.broadcast, rfq.quote_accepted, rfq.quote_rejected, rfq.expired, swap.hashlock_shared, swap.canton_locked, swap.eth_locked, swap.eth_claimed, swap.canton_claimed, swap.completed, swap.refunded, swap.timelock_expiring, swap.reorg_detected.

{
  "callbackUrl": "https://lp.example.com/hooks/interstice",
  "events": ["rfq.quote_accepted", "swap.completed", "swap.refunded"]
}
Response — 201
{ "id": "wh_1a2b3c4d", "secret": "whsec_9f2a1b3c5e6f7a8b9c0d1e2f3a4b5c6d" }
GET/webhooks

List your webhook registrations (secrets never included).

Response — 200
{
  "data": [
    { "id": "wh_1a2b3c4d", "callbackUrl": "https://lp.example.com/hooks/interstice", "events": ["rfq.quote_accepted", "swap.completed"] }
  ]
}
DELETE/webhooks/:id

Remove a registration.

Path params
FieldType
idstring — webhook id
Response — 204

No body.

POST/webhooks/:id/rotate-secret

Rotate the signing secret — returned once, in the response.

Path params
FieldType
idstring — webhook id
Response — 200
{ "secret": "whsec_5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f" }
GET/webhooks/deliveries

Your delivery log.

Query params
FieldType
cursorstring
limitinteger
statusstring
Response — 200
FieldType
data[].eventIdstring
data[].idempotencyKeystring
data[].eventTypestring — see event types above
data[].swapId / rfqIdstring, optional
data[].entityIdstring
data[].dataobject — event payload
data[].timestampISO-8601 string
{
  "data": [
    {
      "eventId": "evt_7e8f9a0b",
      "idempotencyKey": "idem_7e8f9a0b",
      "eventType": "swap.completed",
      "swapId": "swap_7e4c1a2b",
      "entityId": "cp_9f2a1b3c",
      "data": { "state": "COMPLETED" },
      "timestamp": "2026-07-02T14:12:41Z"
    }
  ],
  "cursor": null,
  "hasMore": false
}
POST/webhooks/deliveries/:id/replay

Re-deliver a webhook event.

Path params
FieldType
idstring — delivery id
Response — 202

No body — replay queued.

How an LP integrates

Getting a price to a taker works one of two ways, depending on how your systems are set up. Everything after a quote is submitted — offer forwarding, acceptance, and settlement — is the same regardless of which mode you use.

Poll for requests

Your systems call GET /rfqs on your own schedule, price the requests you're eligible for and want to quote, and submit offers with POST /rfqs/:id/quote. Straightforward to integrate — no inbound connectivity required on your side.

Receive requests directly

Interstice sends the request terms to an endpoint you host, and your quoting engine responds in-line with a price. Closer to a standard RFQ/RFS pattern — lower latency, but requires you to expose and maintain a reachable endpoint with your own response-time guarantees.

1
Get the request

Via polling (GET /rfqs) or a direct call to your registered endpoint, depending on your chosen mode.

2
Price it

Your own market-making logic determines the rate you're willing to offer.

3
Submit your offer

Via POST /rfqs/:id/quote, or as your direct response, depending on mode.

4
Offer reaches the taker

The taker sees your offer alongside others and decides whether to accept.

5
Acceptance creates a swap

Once accepted, a swap is created that you can retrieve via GET /swaps or via a webhook, depending on your subscriptions.

6
Settle

You drive your side of settlement through the same API — see below.

Settlement flow

Once a swap exists, both sides lock and claim funds on their respective chains under a shared hashlock. Each on-chain action is followed by a report call so Interstice's verified state machine can advance — Interstice independently checks each report against the relevant chain before accepting it.

StepActionEndpoint(s)
1 Agree the hashlock for the swap POST /swaps/:id/report-hashlock
2 Lock funds on your chain leg — Canton: prepare then submit your signed transaction. Ethereum: sign and broadcast with your own infrastructure (no Interstice endpoint for the transaction itself). POST /swaps/:id/canton/prepare
POST /swaps/:id/canton/execute
3 Report your lock — use report-htlc-created for your first lock on a quote, keyed by commitment since the swap record doesn't exist yet; use the per-chain report for the leg you locked. POST /swaps/by-commitment/:commitment/report-htlc-created
POST /swaps/:id/report-canton-lock
POST /swaps/:id/report-eth-lock
4 Counterparty locks and reports their own leg — no action required from you here.
5 If a lock's timelock is close to expiry before the counterparty leg completes, extend it. POST /swaps/:id/report-htlc-refresh
6 Claim by revealing the preimage on-chain, then report the claim for the leg you claimed. POST /swaps/:id/report-canton-claim
POST /swaps/:id/report-eth-claim
7 If a timelock expires before claim, refund instead. POST /swaps/:id/report-refund

Canton-side signing is thin-client: Interstice prepares the unsigned transaction, you sign only the hash, and Interstice never holds your key. Your Ethereum-side leg is signed entirely by your own infrastructure.

Networks & assets

Interstice settles swaps between Canton Network and Ethereum. Supported Ethereum networks include Ethereum mainnet and Sepolia testnet, matched to your onboarding environment. Canton and Ethereum contract identifiers are issued per environment during onboarding — see Base URL & environments.

Security model