Skip to main content
A machine signer lets your backend join a non-custodial wallet’s signing quorum the same way a human signer does - by producing a cryptographic approval - but with a server-held P-256 key instead of a passkey. Your backend discovers the payouts awaiting its signature, stamps each one, and submits the stamp to Conduit. Conduit verifies the stamp, records your vote, and moves the payout forward. This guide covers the signing loop only — wallet provisioning and the valid roster shapes per signing mode are in Add a Crypto Wallet; the concepts behind rosters and quorums live in Multi-signer wallets and Non-Custodial Wallets.
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.
Unattended mode is not enabled on request alone. Because it puts no human on your side of the quorum, Conduit enables it per customer only after an additional approval review. Plan for that review before you build against it; the other programmatic mode has no such gate.
Conduit is always a required co-approver, in every mode. Reaching your own signing threshold is necessary but never sufficient: Conduit’s compliance approval is the final vote on every withdrawal, and it is cast last. Your machines can never move money on their own, and Conduit can never move it without you - neither side acts alone. “Programmatic unattended” removes the human from your side of the quorum; it does not remove Conduit’s compliance check.
No gas to manage. Network fees on non-custodial wallets are sponsored for you - the wallet never needs to hold a native-gas balance, and you never fund one.

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 with 02 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 active inline: 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 (verificationUrl with urlAudience: "admin", also delivered on the wallet_ceremony.awaiting_admin_approval webhook), and the new machine signer stays pending until an admin co-approves it; it goes active only once that co-approval clears.
  • When the roster carries an active api_key admin, the same response ADDS a machineApproval object alongside that link and webhook — a shortcut so you can approve without a human opening the link. machineApproval carries a token and the approvalMaterial to stamp. You stamp that material with one of your active api_key admin keys exactly as in step 4, then POST it to /v2/verifications/{token}/complete to 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 no machineApproval — 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 an active api_key admin on this roster — a non-admin or wrong-scheme stamp is rejected by the same admissibility gate the payout approve path uses.
A machine signer has no passkey enrollment step: it becomes 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 the transaction.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.
The 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 its approvalMaterial, 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).
A rejection is terminal and immediate. A single reject ends the payout - it cannot be revived, and no further approvals will complete it. There is no “undo”: to send that payout you originate a new one. Gate which of your machine signers may reject.

Outcomes

Fetch GET /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.

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.