Start here
Bring a key. Keep it server-side.
Workspace API keys are provisioned out of band and stored hashed at rest. Send one as Authorization: Bearer …. Never put it in a query string, browser bundle, log, or MCP tool input.
Preconfigured Bearer authentication, per-key rate limits, REST v1, and the stateless MCP endpoint are mounted.
A signed-in workspace owner can create a one-time-visible, browser-generated key in Agent access. OAuth discovery, webhook-secret distribution, and live receiver UAT remain unavailable.
REST v1
Six paths. One narrow authority.
/api/v1/acceptancesCreate one draft for an existing workspace job.
- Auth
- Workspace key
- Idempotency
- Required
/api/v1/acceptances/{acceptanceId}/evidenceAttach one photo or document as agent-upload evidence.
- Auth
- Workspace key
- Idempotency
- Required
/api/v1/acceptances/{acceptanceId}/request-signaturesRequest the separate, human-approved signing ceremony.
- Auth
- Workspace key
- Idempotency
- Required
/api/v1/acceptances/{acceptanceId}Read the PII-minimal lifecycle and role progress.
- Auth
- Workspace key
- Idempotency
- —
/api/v1/acceptances/{acceptanceId}/sealedRead an already-committed seal bound to the acceptance.
- Auth
- Workspace key
- Idempotency
- —
/api/v1/verify/{recordId}Read the allowlisted verification facts for a record.
- Auth
- Public
- Idempotency
- —
Mutation examples
Retry the same intent unchanged.
Each mutation requires a stable Idempotency-Key. Reuse it only for the same acceptance intent. A replay returns the original result; changed intent returns 409.
curl --request POST "$SIGNTHATUP_ORIGIN/api/v1/acceptances" \
--header "Authorization: Bearer $SIGNTHATUP_AGENT_KEY" \
--header "Idempotency-Key: acceptance.create.0001" \
--header "Content-Type: application/json" \
--data '{"jobId":"job_existing_0001"}'curl --request POST \
"$SIGNTHATUP_ORIGIN/api/v1/acceptances/acceptance_0001/evidence" \
--header "Authorization: Bearer $SIGNTHATUP_AGENT_KEY" \
--header "Idempotency-Key: evidence.attach.0001" \
--header "Content-Type: application/json" \
--data '{
"bytesBase64":"AQID",
"evidenceId":"evidence_photo_0001",
"kind":"photo",
"mimeType":"image/jpeg",
"recordedAt":"2026-07-17T12:00:00.000Z",
"sequence":0
}'curl --request POST \
"$SIGNTHATUP_ORIGIN/api/v1/acceptances/acceptance_0001/request-signatures" \
--header "Authorization: Bearer $SIGNTHATUP_AGENT_KEY" \
--header "Idempotency-Key: signatures.request.0001"The signature request is deliberately bodyless. It creates an approval-required request—not a signature, OTP, or outbound message. Hand the acceptance ID to the signed-in tradesperson at /acceptances/{acceptanceId}/review. Only that private human review can approve, freeze the evidence, and cause the client verification code to be issued.
Polling model
Read progress without reading people.
- 01Draft
draft - 02Human decision
approval-required · declined · approved - 03Signer verification
awaiting-otp - 04Human signatures
awaiting-signatures · partially-signed - 05Separate minting
ready-to-mint
GET /acceptances/{acceptanceId} returns only job, locale, OTP lifecycle, and client/tradesperson role progress. Read /sealed only after separate minting commits the record.
Outbound lifecycle
Verify bytes first. Process the event once.
SignThatUp delivers canonical, PII-minimal lifecycle envelopes by HTTPS POST. Delivery is at least once: store the x-signthatup-event-id under a unique constraint and make processing idempotent before returning a success response.
Registration, disable, signing, queueing, and guarded delivery are mounted. A receiver still needs operator-approved key transfer, deployment configuration, monitoring, and private UAT.
acceptance.created · acceptance.partially_signed · acceptance.sealed · acceptance.failed
60s · 300s · 900s · 3600s · 10800s · 21600s · 43200s · then terminal
408, 425, 429, 5xx, timeout, and ambiguous connection loss retry.
Redirects are rejected. Response bodies are discarded, not persisted.
content-typeCanonical JSON encoded as UTF-8.
x-signthatup-event-idStable deduplication identity for at-least-once delivery.
x-signthatup-event-typeOne of the four lifecycle event kinds below.
x-signthatup-signatureHMAC-SHA-256 over the exact raw body bytes.
x-signthatup-signing-key-idSelects the retained verification key during rotation.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody is the untouched request body Buffer. Verify before JSON.parse.
const received = request.headers["x-signthatup-signature"]
?.match(/^v1=([A-Za-z0-9_-]{43})$/)?.[1];
const key = Buffer.from(process.env.RECEIVER_WEBHOOK_KEY_BASE64!, "base64");
const expected = createHmac("sha256", key)
.update(rawBody)
.digest("base64url");
if (!received || !timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
throw new Error("invalid webhook signature");
}
// Insert x-signthatup-event-id under a unique constraint, then process once.Select the retained key by x-signthatup-signing-key-idduring rotation. Never log the key, endpoint, raw body, signature, or request headers. Parse JSON only after signature verification.
/api/v1/management/webhooksRegister one HTTPS receiver and a canonical lifecycle-event allowlist.
- Auth
- Workspace browser session · same origin
- Replay
- Required header
/api/v1/management/webhooks/{subscriptionId}Permanently disable one workspace-owned subscription; safe to replay.
- Auth
- Workspace browser session · same origin
- Replay
- Immutable replay
These management paths use the signed-in workspace browser session, not the agent Bearer key. Registration returns a key ID—not signing key bytes. Disabling prevents later sends but cannot retract a request already in flight.
Error taxonomy
Failures stay small and actionable.
401Replace or restore the workspace key.
400 / 413 / 415Fix the exact request before retrying.
429Wait until Retry-After, then retry the same intent.
404Treat missing and foreign resources identically.
409Use a new key only for a genuinely new intent.
503Retry unchanged; do not assume the mutation failed.
MCP server
The same boundary, exposed as tools.
Connect to POST /api/v1/mcp with the workspace key. The server uses stable MCP 2025-11-25 JSON responses, is stateless, and does not offer SSE sessions, resources, prompts, tasks, or OAuth discovery.
POST /api/v1/mcp HTTP/1.1
Authorization: Bearer $SIGNTHATUP_AGENT_KEY
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2025-11-25
{
"jsonrpc":"2.0",
"id":1,
"method":"initialize",
"params":{
"protocolVersion":"2025-11-25",
"capabilities":{},
"clientInfo":{"name":"your-agent","version":"1.0.0"}
}
}create_acceptanceCreate one idempotent acceptance draft for an existing workspace job. This does not sign, contact a signer, or start a ceremony.
attach_evidenceAttach one idempotent photo or document as agent-upload evidence. Bytes remain evidence and never become a signature.
request_signaturesRequest the separate human signature ceremony after approval. This tool cannot create or submit a signature.
get_acceptanceRead the PII-minimal acceptance lifecycle, OTP state, and canonical human role receipt progress.
get_sealed_recordRead an already-committed sealed record cryptographically bound to the acceptance. This never initiates minting.
verify_recordRead the public PII-minimal verification projection for a sealed record identifier.
Human boundary
What an agent can never do.
- Provide or infer signer contact information.
- Issue or verify a signer's OTP.
- Create, upload, or submit a human signature.
- Approve the first ceremony for an unverified contact.
- Initiate minting or claim that a pending record is sealed.
Agents prepare and observe. Humans approve and sign. The sealed proof records which boundary produced every fact.