Counterparty (LP) API Reference
For liquidity providers integrating with the Interstice cross-chain swap engine (Canton Network ⇄ Ethereum).
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
/configEverything needed to self-configure — chain ids, HTLC addresses, Canton package/synchronizer/scan, assets, platform limits.
Response — 200
| Field | Type |
|---|---|
networks[].chainId | integer |
networks[].name | string |
networks[].htlcAddress | string (0x address) |
canton.synchronizerId | string |
canton.packageId | string |
canton.scanUrl | string (url) |
assets[].symbol | string |
assets[].chain | "ethereum" | "canton" |
assets[].decimals | integer |
platform.minSwapUsd / maxSwapUsd | number |
platform.maxFeeBps / maxSlippageBps | integer |
{
"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 }
}
/meYour counterparty profile.
Response — 200
| Field | Type |
|---|---|
entityId | string |
name | string |
status | string |
cantonOnboarded | boolean |
supportedPairs | string[] |
{
"entityId": "cp_9f2a1b3c",
"name": "Example Liquidity LLC",
"status": "ACTIVE",
"cantonOnboarded": true,
"supportedPairs": ["ETH/CC", "USDC/CC"]
}
/me/supported-pairsSet the pairs you're eligible to quote on.
Request body
| Field | Type |
|---|---|
pairs | string[] |
{ "pairs": ["ETH/CC", "USDC/CC"] }
Response — 200
Updated CpProfile — same shape as GET /me above.
/me/feeYour effective fee tier.
Response — 200
| Field | Type |
|---|---|
tier | string |
platformFeeBps | integer |
{ "tier": "partner", "platformFeeBps": 10 }
/me/balancesYour own Canton CC balance.
Response — 200
| Field | Type |
|---|---|
canton.cc | decimal string |
canton.unlocked | decimal string |
{ "canton": { "cc": "15230.75", "unlocked": "12000.00" } }
RFQ & quoting
/rfqsOpen requests you're eligible to quote (privacy-stripped — no requester identity unless anonymized is false).
Query params
| Field | Type | Required |
|---|---|---|
cursor | string | no |
limit | integer | no |
pair | string | no |
direction | string | no |
Response — 200
| Field | Type |
|---|---|
data[].id | string |
data[].direction | "ONRAMP" | "OFFRAMP" |
data[].pair | string, "<evmAsset>/<cantonAsset>" |
data[].origination | { network, asset, amount } |
data[].destination | { network, asset } |
data[].maxSlippageBps | integer |
data[].anonymized | boolean |
data[].requesterId | string or null |
data[].createdAt / expiresAt | ISO-8601 string |
cursor | string or null |
hasMore | boolean |
{
"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
}
/rfqs/:idDetail on a specific request (404 if not found or not eligible).
Path params
| Field | Type |
|---|---|
id | string — RFQ id |
Response — 200
A single CpRfqSummary object — same shape as one item in data[] above.
/rfqs/:id/quoteSubmit an offered rate.
Path params
| Field | Type |
|---|---|
id | string — RFQ id |
Request body
| Field | Type | Notes |
|---|---|---|
offeredRate | decimal string | destination per origination unit |
destinationAmount | decimal string | load-bearing settlement figure |
originationReceiver | string | advisory — server resolves the signing party |
destinationSender | string | |
validUntil | ISO-8601 string |
{
"offeredRate": "2665.40",
"destinationAmount": "3998.10",
"originationReceiver": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
"destinationSender": "cp-lp-party::1220a1b2c3d4e5f6",
"validUntil": "2026-07-02T14:05:00Z"
}
Response — 201
| Field | Type |
|---|---|
quoteId | string |
rfqId | string |
state | ACTIVE | ACCEPTED | REJECTED | EXPIRED | WITHDRAWN |
offeredRate / destinationAmount | decimal string |
createdAt / validUntil | ISO-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.
/rfqs/:id/quote/:quoteIdWithdraw a live quote before it's accepted.
Path params
| Field | Type |
|---|---|
id | string — RFQ id |
quoteId | string — quote id |
Response — 204
No body.
/quotesYour outstanding quotes across all open requests.
Query params
| Field | Type |
|---|---|
cursor | string |
limit | integer |
state | string |
Response — 200
Paginated list of CpQuote — same item shape as the response of POST /rfqs/:id/quote above, wrapped in { data, cursor, hasMore }.
Swaps
/swapsYour swaps.
Query params
| Field | Type |
|---|---|
cursor | string |
limit | integer |
state | string |
Response — 200
| Field | Type |
|---|---|
data[].id | string |
data[].direction | "ONRAMP" | "OFFRAMP" |
data[].pair | string |
data[].state | see swap states below |
data[].hashlock | string (0x, 32-byte hex) |
data[].role | "maker" |
data[].createdAt / updatedAt | ISO-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
}
/swaps/:idFull swap detail. This shape is reused as the response body of every report-* call below.
Path params
| Field | Type |
|---|---|
id | string — swap id |
Response — 200
| Field | Type |
|---|---|
| everything in the swap summary above, plus: | |
origination / destination | { network, asset, amount } |
timelocks.ethExpiresAt / cantonExpiresAt | ISO-8601 string |
fee | { platformFeeBps, feeChain } or null |
legs.ethSwapId | string, optional |
legs.cantonHtlcCid | string, 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" }
}
/swaps/:id/confirmationTrade confirmation for a swap.
Path & query params
| Field | Type |
|---|---|
id | path — string, swap id |
format | query — "json" | "pdf", optional |
Response — 200 (format=json)
| Field | Type |
|---|---|
swapId, direction, pair, state | as above |
origination / destination | { network, asset, amount } |
fee | { platformFeeBps, feeChain } or null |
completedAt | ISO-8601 string or null |
legs.ethTxHash / cantonUpdateId | string, 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).
/swaps/:id/canton/prepareServer builds the unsigned Canton transaction.
Path params
| Field | Type |
|---|---|
id | string — swap id |
Request body
| Field | Type |
|---|---|
action | enum — see below |
params | object, 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
| Field | Type |
|---|---|
preparedTransactions[].hash | base64 string — recompute and compare before signing |
preparedTransactions[].tx | string |
preparedTransactions[].meta | object, optional |
{
"preparedTransactions": [
{
"hash": "cHJlcGFyZWQtdHgtaGFzaC1leGFtcGxl",
"tx": "base64-or-hex-encoded-unsigned-transaction-payload",
"meta": { "step": "lock-canton" }
}
]
}
/swaps/:id/canton/executeSubmit your signed transaction. Interstice relays it — non-custodial.
Path params
| Field | Type |
|---|---|
id | string — swap id |
Request body
| Field | Type |
|---|---|
action | same enum as canton/prepare |
tx | string — the prepared transaction |
signature | base64 string — Ed25519 |
signedBy | string — key fingerprint |
{
"action": "lock-canton",
"tx": "base64-or-hex-encoded-unsigned-transaction-payload",
"signature": "MEUCIQDx3example9c9SIGNATUREbase64ExampleValueOnly8f9a0b==",
"signedBy": "fingerprint_9f2a1b3c"
}
Response — 200
| Field | Type |
|---|---|
updateId | string |
contractId | string, 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.
/swaps/by-commitment/:commitment/report-htlc-createdReport your first lock on a quote, before the swap record exists — keyed by quote commitment, not swap id.
Path params
| Field | Type |
|---|---|
commitment | string — quote commitment (64-hex) |
Request body
| Field | Type |
|---|---|
htlcCid | string, optional |
{ "htlcCid": "00c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9" }
/swaps/:id/report-hashlockReport the agreed hashlock (ONRAMP sets it / OFFRAMP confirms it).
Request body
| Field | Type |
|---|---|
hashlock | string (0x, 32-byte hex) |
{ "hashlock": "0x1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d1a2b3c4d" }
/swaps/:id/report-canton-lockReport a Canton-side lock.
Request body
| Field | Type |
|---|---|
contractId | string, optional |
txId | string, optional |
lockedAmuletEventBlob | string |
lockedAmuletTemplateId | string, optional |
{
"contractId": "00a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5",
"lockedAmuletEventBlob": "eventblob-base64-or-hex-example"
}
/swaps/:id/report-htlc-refreshReport an HTLC contract-id rotation (timelock extension before expiry).
Request body
| Field | Type |
|---|---|
newContractId | string |
txId | string, optional |
lockedAmuletEventBlob | string |
lockedAmuletTemplateId | string, optional |
{
"newContractId": "00b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"lockedAmuletEventBlob": "eventblob-base64-or-hex-example"
}
/swaps/:id/report-eth-lockReport an Ethereum-side lock (OFFRAMP).
Request body
| Field | Type |
|---|---|
txHash | string |
ethSwapId | string |
feeAmount | decimal string, optional |
feeBps | integer, optional |
feeCollector | string (0x address), optional |
{
"txHash": "0x3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f3b152f8f",
"ethSwapId": "0x9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c9f2a1b3c",
"feeAmount": "0.0015",
"feeBps": 10,
"feeCollector": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
}
/swaps/:id/report-eth-claimReport an Ethereum-side claim. The preimage is never sent — it's revealed on-chain by the claim transaction itself.
Request body
| Field | Type |
|---|---|
txHash | string |
{ "txHash": "0x84cc9dfb84cc9dfb84cc9dfb84cc9dfb84cc9dfb84cc9dfb84cc9dfb84cc9dfb" }
/swaps/:id/report-canton-claimReport a Canton-side claim — typically the step that moves a swap to COMPLETED.
Request body
| Field | Type |
|---|---|
contractId | string, optional |
updateId | string, optional |
markerMetadata | object, optional |
{ "contractId": "00c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7", "updateId": "update_1220a1b2c3d4" }
/swaps/:id/report-refundReport a refund of your own leg after its timelock expires.
Request body
| Field | Type |
|---|---|
chain | "ethereum" | "canton" |
txHash | string, optional (Ethereum) |
contractId | string, optional (Canton) |
{ "chain": "ethereum", "txHash": "0x5e6f7a8b5e6f7a8b5e6f7a8b5e6f7a8b5e6f7a8b5e6f7a8b5e6f7a8b5e6f7a8b" }
Webhooks
Event payloads never contain a preimage. Deliveries are signed:
X-Webhook-Signature: sha256=<hmac(rawBody)>.
/webhooksRegister an endpoint. The signing secret is shown once, at registration.
Request body
| Field | Type |
|---|---|
callbackUrl | string (url) |
events | string[] — 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" }
/webhooksList 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"] }
]
}
/webhooks/:idRemove a registration.
Path params
| Field | Type |
|---|---|
id | string — webhook id |
Response — 204
No body.
/webhooks/:id/rotate-secretRotate the signing secret — returned once, in the response.
Path params
| Field | Type |
|---|---|
id | string — webhook id |
Response — 200
{ "secret": "whsec_5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f" }
/webhooks/deliveriesYour delivery log.
Query params
| Field | Type |
|---|---|
cursor | string |
limit | integer |
status | string |
Response — 200
| Field | Type |
|---|---|
data[].eventId | string |
data[].idempotencyKey | string |
data[].eventType | string — see event types above |
data[].swapId / rfqId | string, optional |
data[].entityId | string |
data[].data | object — event payload |
data[].timestamp | ISO-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
}
/webhooks/deliveries/:id/replayRe-deliver a webhook event.
Path params
| Field | Type |
|---|---|
id | string — 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.
Via polling (GET /rfqs) or a direct call to your registered endpoint,
depending on your chosen mode.
Your own market-making logic determines the rate you're willing to offer.
Via POST /rfqs/:id/quote, or as your direct response, depending on mode.
The taker sees your offer alongside others and decides whether to accept.
Once accepted, a swap is created that you can retrieve via GET /swaps or
via a webhook, depending on your subscriptions.
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.
| Step | Action | Endpoint(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/preparePOST /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-createdPOST /swaps/:id/report-canton-lockPOST /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-claimPOST /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
- Entity-scoped keys. Your API key resolves to your organization only — it cannot see or act on any other counterparty's data.
- Deny-by-default surface. Only the routes documented here are reachable; internal and administrative surfaces are not exposed to partners at all.
- Non-custodial settlement. Interstice never holds your keys or funds — it coordinates and verifies, but every on-chain action is signed by you.
- Instant key revocation. Revoking a key takes effect immediately, not on a caching delay.