Test this flow in sandbox first. Drive it end-to-end with simulated money
and deterministic controls - start with the sandbox
quickstart, then the multi-signer
recipes, and the cheat
sheet for every magic value.
Two keys, two jobs
Programmatic signing involves two separate credentials. Keeping them distinct is the whole mental model:
Your API key gets you in the door; your signing key casts the vote. A stamp submitted with a valid API key but an unregistered signing key is still rejected, and vice versa.
Self-custody of the signing key. You generate the machine signing keypair
yourself and keep the private half. Conduit never generates, stores,
transmits, or returns your private key - there is no “get your signing key
from Conduit” step, and no field in any request or response carries a private
key. Conduit only ever learns the public half, which you hand it once at
enrollment. This is what makes a machine signer your signer: only you can
produce a valid stamp.
Who can sign, and what Conduit always keeps
Whether a wallet accepts machine stamps is set by Conduit, out of band, as the customer’s signing mode: an org-wide default with an optional per-customer override (the customer-level setting wins). There is nothing to toggle in the API - arrange it with your Conduit representative.A passkey signer on a programmatic wallet can also approve payouts. A
wallet in a programmatic mode is not machines-only by definition: if its
roster has a human passkey signer, that person is a member of the signing
quorum and may approve a payout on the Conduit-hosted verify page, exactly as
in passkey-required mode. When such a wallet has a payout awaiting signature,
transaction.awaiting_signature carries a verificationUrl (the human link)
alongside the signingRequestId (the machine handle), and the same
payout’s quorum can be met by any mix of machine stamps and human approvals.
This human vote is optional — your machine signers still have to be able to
reach the threshold on their own — so use it for “the bot signs routine
payouts, a person approves the large ones.” A wallet whose programmatic roster
has no passkey signer emits only the signingRequestId.1. Generate a machine signing keypair
You generate the keypair; Conduit only ever learns the public half. Generate a P-256 (secp256r1) keypair in your own tooling, KMS, or HSM and keep the private half in your secret store. There is no step where Conduit issues you a signing key - the key originates on your side, and the private half never leaves your infrastructure (never sent, never stored by us, never returned by any endpoint). A machine signer is exactly that - a machine. Your system holds the private key and stamps approvals programmatically; unlike a human passkey signer, nobody receives an email or opens a page to enroll or to approve. The public key you register must be in compressed SEC1 form: 33 bytes, hex-encoded (66 hex characters), starting with02 or 03. An optional 0x prefix is accepted.
2. Register the public key
Include the machine signer in the roster when you claim the wallet, or add it to an existing roster. You provide only the compressed public key -credentialType: "api_key" plus publicKey (the 33-byte compressed form from step 1). There is no private-key field on this or any endpoint; the private half stays with you. Conduit registers that public key as the signer’s machine credential. A machine signer never has an enrollment link of its own (that flow is only for human passkey signers) — but how it becomes active depends on when you add it:
-
At claim time the machine signer seats
activeinline: it is live as soon as the claim completes, with no approval step. -
Adding one to an already-active roster is a roster change your existing admins must co-approve. The response always returns an admin approval link (
verificationUrlwithurlAudience: "admin", also delivered on thewallet_ceremony.awaiting_admin_approvalwebhook), and the new machine signer stays pending until an admin co-approves it; it goesactiveonly once that co-approval clears. -
When the roster carries an active
api_keyadmin, the same response ADDS amachineApprovalobject alongside that link and webhook — a shortcut so you can approve without a human opening the link.machineApprovalcarries atokenand theapprovalMaterialto stamp. You stamp that material with one of your activeapi_keyadmin keys exactly as in step 4, then POST it to/v2/verifications/{token}/completeto activate the new signer — the same stamp-and-approve step every machine governance ceremony uses. (A roster with only passkey admins gets the link and webhook but nomachineApproval— there is no machine key to stamp with.)Two-party control still holds: your admin’s stamp plus Conduit’s own compliance stamp both apply the change. The stamp must come from anactiveapi_keyadminon this roster — a non-admin or wrong-scheme stamp is rejected by the same admissibility gate the payout approve path uses.
active as soon as the claim completes, and wallet_signer.added fires. The roster still has to satisfy your customer’s signing mode - in programmatic mode a machine member must be role: "signer" and every admin must be a passkey; a mismatch is rejected with SIGNING_MODE_ROSTER_INVALID. Your active machine signers also have to be able to reach the signing threshold on their own, or the claim is rejected with PROGRAMMATIC_QUORUM_UNREACHABLE.
Errors specific to registering the machine public key:
3. Discover payouts awaiting your signature
Event-driven (recommended): react to thetransaction.awaiting_signature webhook. When a programmatic-mode payout reaches the signing step, Conduit fires transaction.awaiting_signature carrying a signingRequestId (plus signingMode; a verificationUrl rides along only when the wallet’s roster has an active passkey signer, and is omitted for a machine-only roster). That event is your trigger — on receipt, fetch that one request with GET /v2/signing-requests/{signingRequestId} (see the note below), build the stamp (step 4), and submit. You do not need to poll for discovery. Subscribe to the exact event transaction.awaiting_signature (selected-mode subscriptions match exact event names; a wildcard like transaction.* is rejected), or subscribe in all mode, on a webhook endpoint.
GET /v2/signing-requests (the list) is the reconciliation view — use it for catch-up on startup, or if you don’t consume webhooks. It returns the open payouts for your organization that are still collecting customer approvals - pending, non-expired, and short of their required approvals. A payout that has already collected quorum, expired, or belongs to a passkey-required wallet never appears here. It is an organization-wide feed keyed to the payout’s state, not to your key: a request you have already stamped stays listed until the full quorum is met, so treat the list as “still collecting approvals,” not “not yet stamped by you.” Re-submitting a stamp you already sent is a safe no-op that returns the request’s current state, so acting on a still-listed request you already approved does no harm.
id addresses the request in the next step, and everything you need to build the stamp is in approvalMaterial. Responses are never cached (Cache-Control: no-store), so each fetch returns current state. Use outbound.toAddress and outbound.assetAmount to confirm you are approving the payout you expect before you stamp it.
GET /v2/signing-requests/{id} returns a single request by its id and the
same shape. Use it to confirm one payout’s status after you submit, when you
need a synchronous read; routine progress arrives on the webhook. An id that
isn’t one of your requests returns 404 SIGNING_REQUEST_NOT_FOUND - the same
response a request in another account would give, so a 404 never reveals
whether an id exists elsewhere.4. Build the stamp and submit it
For each request, build the exact activity body from itsapprovalMaterial, sign that body with your machine key, and POST the body plus the stamp back to Conduit.
The body and the stamp use a few fixed protocol constants (ACTIVITY_TYPE_APPROVE_ACTIVITY, the signature scheme). Copy them verbatim - Conduit re-derives your signature over the exact bytes you send and checks that the body authorizes this specific request (its fingerprint, subOrganizationId, and activity type). Change a byte and the stamp is rejected.
The signed body:
timestampMs is the current time in milliseconds; it must be fresh (a stamp minted long in the past or future is refused). Use ACTIVITY_TYPE_REJECT_ACTIVITY to reject instead of approve. A complete, dependency-free reference implementation in TypeScript:
POST /v2/signing-requests/{id}/approve returns 200 with the request in its updated state - approvedCount incremented, and status: "quorum_met" once your side of the quorum is complete (the payout then moves to Conduit’s compliance approval). Submitting the same stamp again is safe and idempotent: whether the request is still open or has already resolved (quorum met, rejected, or expired), a resubmit returns the request’s current state at 200, never an error - so a machine that timed out mid-submit can simply retry, and you re-read the request to see the final outcome rather than treating a resubmit as a failure.
Rejecting a payout
POST /v2/signing-requests/{id}/reject submits a rejecting stamp (build the body with ACTIVITY_TYPE_REJECT_ACTIVITY).
Outcomes
FetchGET /v2/signing-requests/{id} to follow a request to its terminal state:
Error reference
What Conduit records
Your approval is recorded as a vote attributed to the signer whose key produced the stamp - that vote is the authoritative record of your approval. Conduit records an intent before it relays to the provider, then finalizes it with the provider’s outcome after; the provider’s decision is authoritative, so once it accepts your vote that vote stands even if a later local step retries. Your raw stamp is never stored: it is a bearer signature, so Conduit relays it and discards it. Conduit also keeps a supplementary audit trail (a hash of the body you signed, a fingerprint of the public key that signed it, and which of your API keys submitted it) for operational traceability.Related
Add a Crypto Wallet
Provision the wallet: feature request, claim, roster shapes per signing
mode, activation.
Multi-signer wallets
Roster rules, thresholds, root quorum, and lifecycle endpoints.
Non-Custodial Wallets
The custody model and the hosted verify page for passkey signers.
Sandbox multi-signer recipes
Drive rosters and signing flows deterministically in sandbox.