SignThatUpREST v1 · MCP 2025-11-25

Developer reference

Agent work in. Human signatures only.

Create acceptance drafts, attach evidence, request a human ceremony, and read proof state through one workspace-scoped contract.

Transport
HTTPS JSON · Streamable HTTP
Authentication
Pre-issued workspace Bearer key
Data posture
Private no-store · PII-minimal reads
01

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.

Available now

Preconfigured Bearer authentication, per-key rate limits, REST v1, and the stateless MCP endpoint are mounted.

Owner activation

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.

02

REST v1

Six paths. One narrow authority.

POST/api/v1/acceptances

Create one draft for an existing workspace job.

Auth
Workspace key
Idempotency
Required
POST/api/v1/acceptances/{acceptanceId}/evidence

Attach one photo or document as agent-upload evidence.

Auth
Workspace key
Idempotency
Required
POST/api/v1/acceptances/{acceptanceId}/request-signatures

Request the separate, human-approved signing ceremony.

Auth
Workspace key
Idempotency
Required
GET/api/v1/acceptances/{acceptanceId}

Read the PII-minimal lifecycle and role progress.

Auth
Workspace key
Idempotency
GET/api/v1/acceptances/{acceptanceId}/sealed

Read an already-committed seal bound to the acceptance.

Auth
Workspace key
Idempotency
GET/api/v1/verify/{recordId}

Read the allowlisted verification facts for a record.

Auth
Public
Idempotency
03

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.

Create an acceptance
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"}'
Attach a photo
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
  }'
Request human signatures
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.

04

Polling model

Read progress without reading people.

  1. 01
    Draftdraft
  2. 02
    Human decisionapproval-required · declined · approved
  3. 03
    Signer verificationawaiting-otp
  4. 04
    Human signaturesawaiting-signatures · partially-signed
  5. 05
    Separate mintingready-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.

05

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.

Mounted source · deployment gated

Registration, disable, signing, queueing, and guarded delivery are mounted. A receiver still needs operator-approved key transfer, deployment configuration, monitoring, and private UAT.

Events4 lifecycle facts

acceptance.created · acceptance.partially_signed · acceptance.sealed · acceptance.failed

Attempts8 resolved attempts maximum

60s · 300s · 900s · 3600s · 10800s · 21600s · 43200s · then terminal

SuccessAny 2xx response

408, 425, 429, 5xx, timeout, and ambiguous connection loss retry.

Receiver budget10 seconds · 8 KiB response

Redirects are rejected. Response bodies are discarded, not persisted.

01
content-type

Canonical JSON encoded as UTF-8.

application/json
02
x-signthatup-event-id

Stable deduplication identity for at-least-once delivery.

Opaque event ID
03
x-signthatup-event-type

One of the four lifecycle event kinds below.

Lifecycle kind
04
x-signthatup-signature

HMAC-SHA-256 over the exact raw body bytes.

v1=<base64url digest>
05
x-signthatup-signing-key-id

Selects the retained verification key during rotation.

Opaque key ID
Node receiver verification
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.

POST/api/v1/management/webhooks

Register one HTTPS receiver and a canonical lifecycle-event allowlist.

Auth
Workspace browser session · same origin
Replay
Required header
DELETE/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.

06

Error taxonomy

Failures stay small and actionable.

401
Missing, malformed, revoked, or unknown credential

Replace or restore the workspace key.

400 / 413 / 415
Invalid shape, size, or media type

Fix the exact request before retrying.

429
Per-key rate limit reached

Wait until Retry-After, then retry the same intent.

404
Resource unavailable in this scope

Treat missing and foreign resources identically.

409
Idempotency key conflicts with prior intent

Use a new key only for a genuinely new intent.

503
Temporary configuration or dependency failure

Retry unchanged; do not assume the mutation failed.

07

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.

Initialize MCP
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"}
  }
}
01
create_acceptance

Create one idempotent acceptance draft for an existing workspace job. This does not sign, contact a signer, or start a ceremony.

Idempotent write
02
attach_evidence

Attach one idempotent photo or document as agent-upload evidence. Bytes remain evidence and never become a signature.

Idempotent write
03
request_signatures

Request the separate human signature ceremony after approval. This tool cannot create or submit a signature.

Idempotent write
04
get_acceptance

Read the PII-minimal acceptance lifecycle, OTP state, and canonical human role receipt progress.

Read
05
get_sealed_record

Read an already-committed sealed record cryptographically bound to the acceptance. This never initiates minting.

Read
06
verify_record

Read the public PII-minimal verification projection for a sealed record identifier.

Read
08

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.