Documentation
Cosignet docs
Cosignet provides Critical Action Control for automation, agents, and backend workflows.
Your system requests approval for an exact action payload; an approver signs it with a passkey;
the WebAuthn assertion is cryptographically bound to that payload via the challenge
nonce ‖ SHA-256(payload).
Quickstart — five minutes to first approved
The activation metric for Cosignet is first approved critical action from external code: an approval request created by your own agent or script that an approver signs.
-
1. Get an invite and register an approver passkey
Registration on cosignet.com is invite-only during early access. Ask an admin to create an invite code via the admin API:
curl -s -X POST https://cosignet.com/api/invites \ -H 'content-type: application/json' \ -H "X-Api-Key: $COSIGNET_API_KEY" \ --data '{"note":"alice","maxUses":1}' # => { "code": "…", "registerUrl": "https://cosignet.com/register?invite=…" }Open the
registerUrl, choose a username, and create a resident passkey with biometrics. User verification is required.The public demo at cosignet.com does not require an account.
-
2. Create an approval request
curl -s -X POST https://cosignet.com/api/confirmations \ -H 'content-type: application/json' \ -H "X-Api-Key: $COSIGNET_API_KEY" \ --data '{"username":"alex","action":"Deploy to production", "payload":{"service":"api","sha":"abc123"}}'The response contains an
id, a confirmationurl, thepayloadHash, andexpiresAt. -
3. Approve and read the result
Open the
url, review the action card and its SHA-256, and approve it with a passkey. Then poll — or long-poll with?waitso the request blocks until the decision lands (no inbound webhook, port, or public IP needed; your client just keeps an outbound connection open):# long-poll: returns as soon as the human decides, or after `wait` seconds curl -s "https://cosignet.com/api/confirmations/$id?wait=25" -H "X-Api-Key: $COSIGNET_API_KEY" # => { "status": "approved", "payloadHash": "…", "rawAssertion": { … } }
MCP server reference
The remote MCP endpoint is https://cosignet.com/mcp over streamable HTTP. Send your
API key as the X-Api-Key header.
claude mcp add --transport http cosignet https://cosignet.com/mcp \
--header "X-Api-Key: $COSIGNET_API_KEY"
Tool: request_human_approval
Call before any irreversible, privileged, or risky action. The payload must contain the exact operation parameters so the biometric signature binds to that operation.
action— human-readable label, e.g. "Deploy to production".payload— exact operation object, SHA-256 hashed and signed by the approver.approver_username— Cosignet username of the approver.wait_seconds— optional, 0–25. How long to poll before returningpending.
Returns approved / rejected / expired / pending,
the payload hash, and the confirmation URL.
Tool: get_approval_status
Fetch the current status of a request by id to resume polling or audit a decision.
REST API reference
All /api/* routes require the X-Api-Key header.
POST /api/confirmations
Create an approval request.
{
"username": "alex", // required, an existing approver
"action": "Deploy to production", // required, shown to the human
"payload": { "service": "api" }, // required, exact action object
"ttlSeconds": 180, // optional, 1–86400
"notify": "none" // optional, "none" | "telegram"
}
Response: { id, url, status: "pending", payloadHash, expiresAt }.
GET /api/confirmations/:id
Read status. Response includes status, action, payloadHash,
expiresAt, signedAt, credentialId, and the raw WebAuthn
rawAssertion once approved. Optional query ?wait=0–25 long-polls:
the request holds open until the status leaves pending or the wait elapses, so
clients behind NAT/firewalls get the decision over their own outbound connection.
Admin invite routes
These routes are protected by X-Api-Key and manage invite-only registration.
POST /api/invites— create an invite. Body:{ note?, maxUses?, pinnedUsername?, expiresAt? }. Returns{ code, registerUrl }.GET /api/invites— list all invite codes with usage counts.POST /api/invites/:code/revoke— immediately invalidate a code.
Public demo endpoint
POST /demo/confirmations is unauthenticated and exists only for the landing demo. It
accepts a demo-* username, fixes the payload to
{"action":"demo","amount":"$0"}, and is hard rate limited per IP and globally. It cannot
create confirmations for real accounts.
SDK & recipes
TypeScript SDK
A thin, zero-dependency client wraps the REST API and ships a webhook signature verifier. It runs on Node 18+, Cloudflare Workers, Deno, and browsers.
import { Cosignet } from '@cosignet/sdk';
const cosignet = new Cosignet({ apiKey: process.env.COSIGNET_API_KEY });
const decision = await cosignet.requestApproval(
{ username: 'alex', action: 'Wire transfer to vendor',
payload: { to: 'acct_8821', amount_usd: 4200 }, notify: 'telegram' },
{ onCreated: (c) => console.log('Approve here:', c.url) },
);
// decision.status === 'approved' | 'rejected' | 'expired' | 'pending'
requestApproval long-polls over your own outbound connection,
so it works behind NAT/firewalls. Verify webhooks with
verifyWebhookSignature({ body, signature, secret }) — it mirrors
the server's hex HMAC-SHA256 of the raw body.
The SDK lives in sdk/ in the repo; npm publish is
pending. Vendor sdk/src/index.ts or build it locally for now.
Recipe: biometric approval for dangerous CLI commands
cosignet-guard.sh gates any command behind a biometric
approval, sending the exact argv to a human and running it only once
approved — no inbound ports needed.
COSIGNET_API_KEY=… ./cosignet-guard.sh \
--user alex --action "terraform destroy — prod" \
-- terraform destroy -auto-approve
Find it in recipes/cli-approval/. Works for any high-stakes
command: DROP DATABASE, deleting backup buckets, force pushes.
Recipe: approval gate in CI/CD (no agent)
Block a pipeline at a prod deploy or migration and require a passkey approval bound to the exact deploy facts (commit, ref, environment, actor). The runner long-polls outbound, so it works on hosted and locked-down self-hosted runners — no inbound webhook needed.
COSIGNET_API_KEY=… COSIGNET_USER=alex ./recipes/ci-cd/gate.sh \
--action "Deploy acme/api → production" \
--field environment=production --field commit="$GITHUB_SHA"
Find it in recipes/ci-cd/ with ready-to-copy
github-actions.yml and gitlab-ci.yml jobs.
Recipe: approval gate inside a backend service
Pause an irreversible operation in an automated service — a refund, payout, or bulk delete — until a human signs the exact parameters. Ships a dependency-free Python client (stdlib only) plus a runnable example.
decision = cosignet.request_approval(
username="finance-ops", action="Refund customer",
payload={"customer_id": "cus_8821", "amount_usd": 4200})
if decision["status"] != "approved": raise ApprovalError(decision["status"])
Find it in recipes/backend-service/. The same two-call REST
pattern ports to Go, Ruby, Java, etc.
Webhooks
Register a URL to receive a POST callback whenever a confirmation reaches a terminal state.
All webhook routes require X-Api-Key.
Events
approved— the approver signed the confirmation with a passkey.rejected— a human explicitly rejected the confirmation.expired— the TTL elapsed without a decision.
Register a webhook
curl -s -X POST https://cosignet.com/api/webhooks \
-H 'content-type: application/json' \
-H "X-Api-Key: $COSIGNET_API_KEY" \
--data '{"url":"https://your-server.example/hook","events":["approved","rejected","expired"]}'
# => { "id": "…", "url": "…", "events": […] }
List, revoke, and inspect deliveries
GET /api/webhooks— list all registered webhooks.POST /api/webhooks/:id/revoke— revoke a webhook.GET /api/webhook-deliveries?webhookId=:id— list delivery attempts. Optional filters:webhookId,confirmationId,status.
Request headers on each delivery
Cosignet-Event— the event name.Cosignet-Delivery-Id— stable ID for this webhook delivery; use for idempotency across retries.Cosignet-Timestamp— ISO 8601 UTC timestamp of the attempt.Cosignet-Signature— HMAC-SHA-256 hex over the raw request body, keyed by your webhook secret.
Verifying the signature
Compute HMAC-SHA-256 over the exact raw request body bytes using your webhook secret and compare
it to Cosignet-Signature. Reject requests where Cosignet-Timestamp is more than a few
minutes old to limit replay exposure. Use Cosignet-Delivery-Id to deduplicate retried deliveries.
import { createHmac } from 'node:crypto';
function verifyWebhook(rawBody, secret, headers) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
if (expected !== headers['cosignet-signature']) throw new Error('bad signature');
const ts = new Date(headers['cosignet-timestamp']);
if (Math.abs(Date.now() - ts.getTime()) > 5 * 60 * 1000) throw new Error('timestamp out of window');
}
Retry schedule
A 2xx response marks the delivery delivered. Any other status or network error triggers a retry
on the following schedule. After all retries are exhausted the delivery moves to dead_letter.
| Attempt | Delay from previous |
|---|---|
| 1 (initial) | immediate |
| 2 | +30 s |
| 3 | +2 min |
| 4 | +10 min |
| 5 | +1 h |
| 6 | +6 h |
| — | → dead_letter |
Security model
The binding
The WebAuthn authentication challenge is 16 random bytes ‖ SHA-256(payload JSON). The
nonce gives replay protection; the hash binds the assertion to the exact action. What the human saw
is what got signed.
Guarantees
- Required user verification (biometrics or device PIN) on every approval.
- WebAuthn phishing resistance where the RP ID and origin are enforced by the browser.
- Payload binding through the challenge construction above.
- An audit trail: the raw assertion is stored as proof of who approved what.
- Replay protection: each approval consumes its one-time challenge; reuse returns
409.
Non-guarantees
- No hardware attestation for synced passkeys; a synced credential can exist on multiple devices.
- Cosignet binds approval to an action; it does not execute the action or enforce that your system runs only the approved payload.
- No absolute security guarantees are made. The signature cannot be forged without the user's device, and the action cannot be swapped after approval without breaking the binding.
Self-check — verify challenge binding locally
This script recomputes the payload-binding half of the challenge from a confirmation's payload and
compares it to the SHA-256 reported by the API. It proves the action you fetched is the action whose
hash was advertised. Full WebAuthn verification additionally requires parsing the
clientDataJSON (whose challenge field must equal
base64url(nonce ‖ SHA-256(payload))), checking authenticatorData RP ID hash
and the UV flag, and verifying the signature against the stored credential public key — done
server-side by @simplewebauthn/server.
// node selfcheck.mjs <id> (needs COSIGNET_API_KEY)
import { createHash } from 'node:crypto';
const base = process.env.COSIGNET_BASE_URL ?? 'https://cosignet.com';
const id = process.argv[2];
const key = process.env.COSIGNET_API_KEY;
const data = await fetch(`${base}/confirm/${id}/data`).then((r) => r.json());
const status = await fetch(`${base}/api/confirmations/${id}`, {
headers: { 'X-Api-Key': key },
}).then((r) => r.json());
// Recompute SHA-256 over the exact payload JSON returned by the server.
const localHash = createHash('sha256').update(JSON.stringify(data.payload)).digest('hex');
console.log('action :', data.action);
console.log('payload :', JSON.stringify(data.payload));
console.log('api hash :', status.payloadHash);
console.log('recomputed :', localHash);
console.log('binding match:', localHash === status.payloadHash);
// Honest limit: this checks payload→hash binding. It does not by itself verify
// the WebAuthn signature; that requires the stored credential public key and the
// full assertion (clientDataJSON, authenticatorData, signature).
if (status.rawAssertion) {
const cdj = JSON.parse(
Buffer.from(status.rawAssertion.response.clientDataJSON, 'base64url').toString('utf8'),
);
console.log('cdj type :', cdj.type, '(expect webauthn.get)');
console.log('cdj origin :', cdj.origin);
}
The server stores the raw assertion exactly as received so this proof is reproducible.