qub-API-referentie

qub API — OpenAPI 1.0.0

API for qub, a timed commitment and timed publication system backed by drand timelock encryption. Seal qubs now, reveal them later.

Base URL(s):

Authentication

turnstile — Cloudflare Turnstile verification token. Despite this being listed as a bearer scheme, the token is passed in the request body as turnstile_token, not in the Authorization header.

apiKey — API key with prefix qub_sk_. Pass in the Authorization header as Bearer qub_sk_....

Endpoints

qubs

Create, seal, and read qubs

GET /api/v1/qub/{tx_id} — Read stored qub bytes

Auth: public (optional API key)

Returns the stored qub bytes plus minimal storage metadata. Private deliveries are OuterWrapper CBOR and require the URL-fragment key K for local unwrap; public deliveries are bare SealedQubCbor and need no K. The Worker does not decrypt this response.

Rate limiting. Anonymous (no API key) and free-tier API key callers share a 30-requests-per-minute per-IP bucket (over-quota returns 429 with code: RATE_LIMIT_IP). API-key callers whose key record carries rate_limit > 30 get a per-key bucket sized to that value (Builder default 60/min; over-quota returns 429 with code: RATE_LIMIT_KEY). Per-key buckets prevent a single noisy IP from exhausting a paid customer's budget.

Read quota (Builder). Builder API-key callers are NOT subject to a hard read cap — reads past the in-base allowance (100,000/month) accrue against the metered qub_builder_read_overage Stripe price ($0.50 per 100,000 reads). Enterprise / legacy keys still hit the per-day max_reads_per_day cap and receive 429 READS_EXHAUSTED on exhaustion.

Optional charge cap. Builder customers can opt into a monthly charge ceiling via PATCH /api/v1/api-keys/{keyHash} (max_monthly_charge_usd field). When set, reads (and seals) reject with 402 MONTHLY_CAP_REACHED once the projected end-of-period overage charge would exceed the cap.

Parameters

Name In Required Type Description
tx_id path yes string Storage transaction ID.

Responses

Status Description Body
200 Wrapped-private or bare-public qub bytes plus minimal metadata. QubReadResponse (application/json)
404 qub not found. Error (application/json)
429 Rate limited. Error (application/json)
451 Content denied by moderation. Error (application/json)
502 Upstream failure fetching from permanent storage or drand. Error (application/json)

200 response body — QubReadResponse

Field Type Required Description
tx_id string yes
wrapped_cbor_base64 string yes Base64-encoded stored bytes: canonical OuterWrapper CBOR for private delivery or bare canonical SealedQubCbor for public delivery.
arweave_block_timestamp number no Storage block timestamp (Unix seconds UTC). Best-effort — absent for unconfirmed transactions.
intent string no Compose intent that the creator selected (if any), read from the Intent storage tag. Used by the viewer's 'Seal your own qub' CTA.

GET /api/v1/qub/{tx_id}/bytes — Raw stored qub artifact bytes

Auth: public

Returns the raw stored artifact for a qub: private OuterWrapper CBOR or public bare SealedQubCbor. Uses R2 cache-aside and immutable caching (Cache-Control: public, max-age=31536000, immutable). The in-browser embed and SPA resolve the delivery shape, then perform client-side tlock decryption without redundant storage-gateway hits. Rate limited to 120 requests per minute per IP — higher than the JSON /api/v1/qub/{tx_id} endpoint because each viewer/embed page-load typically issues one bytes request to refresh the cache.

Parameters

Name In Required Type Description
tx_id path yes string

Responses

Status Description Body
200 Raw stored artifact bytes: private OuterWrapper CBOR or public bare SealedQubCbor. string (binary) (application/octet-stream)
404 qub not found in permanent storage. Error (application/json)
429 Rate limited (120 / IP / minute). Error (application/json)

GET /api/v1/qub/{tx_id}/meta — Lightweight qub metadata (no decrypt)

Auth: public

Returns the storage block timestamp and intent tag for a sealed qub. No tlock decrypt, no stored-bytes fetch — just a GraphQL lookup with R2 cache. Used by the viewer countdown screen, the ?from={intent} viral-loop CTA, and the <qub-embed> iframe. The qub.social viewer and the embed iframe both re-poll this endpoint every 30 seconds during countdown so the watching count climbs live; polling halts in the last minute before unlock. Rate limited 10 requests per minute per IP.

Parameters

Name In Required Type Description
tx_id path yes string Storage transaction ID.

Responses

Status Description Body
200 Metadata. Both fields are optional — empty {} means the GraphQL fetch failed or the transaction isn't yet confirmed in a block. Always 200, never 404. QubMetaResponse (application/json)
400 Invalid tx_id. Error (application/json)

200 response body — QubMetaResponse

Field Type Required Description
arweave_block_timestamp integer no Unix seconds when the qub's storage transaction was first included in a block.
intent enum ("announcement" / "thesis" / "prediction" / "letter" / "secret" / "commitment" / "proof" / "verdict") no Intent tag set at compose time. One of eight canonical framings — seven user-selectable from the compose pill row plus verdict, the system-emitted intent for a chained creator self-grading qub.
parent_tx_id string no Parent qub's storage tx_id (43-char base64url) when this qub was sealed as a reply via ?reply_to=<parent_tx>. Read from the Parent-Tx-Id storage tag. Absent for non-reply qubs. Used by the viewer reveal page to render a 'Replied to {parent}' back-link without a server-side reverse index.
author_fingerprint string no Lowercase 64-char hex fingerprint of the creator's signing pubkey. Read from the Author storage tag. Absent by default — under privacy-by-default, the Author tag is opt-in per qub at seal time. Present only on qubs the creator chose to attribute publicly; the viewer countdown then renders 'Sealed by @handle' after attestation lookup, and falls back to 'Sealed anonymously' (or no author line) when this field is absent. — pattern: ^[0-9a-f]{64}$
watching integer no Current watching counter for the qub. Read from the watch:<tx_id> KV counter. Both the qub.social viewer and the embed iframe re-poll this endpoint every 30s during countdown so the figure climbs live; polling halts in the final minute before unlock to avoid racing the reveal transition. Absent if the counter read failed transiently.
reactions object no Aggregate reaction tallies for a revealed prediction qub. Absent for unrevealed qubs and for qubs with no reactions yet.
denylisted boolean no True if the qub has been removed from public viewing via the moderation denylist. The F4 embed iframe checks this before fetching stored bytes.

GET /api/v1/qub/{tx_id}/proof — Transparency-log inclusion proof

Auth: public (optional API key)

Returns the canonical-CBOR inclusion proof for an anchored qub (PROTOCOL.md §16.9): the exact leaf bytes, the leaf index, the anchored tree size, the RFC 6962 audit path, the committed Merkle root, and the AnchorRef (the Arweave anchor that committed that root). A standalone verifier (tools/qub-verify --anchor) recomputes the leaf hash, folds the audit path, and checks the result against the anchor — it never trusts a supplied hash. Proofs resolve from the permanent R2 substrate plus the anchor, so reclaiming LogDO storage never invalidates one. Cached public, max-age=3600.

Parameters

Name In Required Type Description
tx_id path yes string Arweave transaction id (base64url, 1-64 chars, at least 8 distinct characters).

Responses

Status Description Body
200 Canonical-CBOR InclusionProof (leaf, index, size, audit path, committed root, AnchorRef). string (binary) (application/cbor)
400 Malformed tx_id. Error (application/json)
404 Unknown tx_id, insufficient entropy, or the qub is not yet anchored. Error (application/json)
429 Rate limited. Error (application/json)
503 Transparency-log store unbound, or the store advanced past the anchored size mid-window — retry. Error (application/json)

POST /api/v1/seal — Server-side seal (Builder)

Auth: API key

Seals a plaintext qub server-side using drand timelock encryption, applies the AES-256-GCM outer wrapper, and durably schedules the exact signed transaction for permanent storage. Requires an API key with the seal scope, a strong Idempotency-Key, and a caller-generated 32-byte wrapper key in wrapper_key_b64url; subject to the API key's IP allowlist if configured. Trust-model note: the plaintext body and K pass through the Worker in memory (neither is persisted), unlike the client-side /api/v1/upload path. Body cap 50 KB. Maximum unlock horizon 10 years from now. Generate and retain K before the request. The response returns the same K for convenience, but the replay store deliberately strips it; after a lost first response, combine the replayed tx URL with the caller-retained #K. Per-API-key seal rate limits and per-period quotas are enforced.

Rate-limit / ceiling response codes (429):

Idempotency-Key is mandatory because sealing is billable and irreversible. Same-body retries replay the result for 24h instead of creating a second qub; incompatible key reuse returns IDEMPOTENCY_KEY_CONFLICT, missing strong authority returns IDEMPOTENCY_UNAVAILABLE, and a missing header returns IDEMPOTENCY_KEY_REQUIRED. The header MUST match ^[A-Za-z0-9._-]{1,255}$. Concurrent or ambiguity-fenced retries return IDEMPOTENCY_IN_FLIGHT.

Parameters

Name In Required Type Description
Idempotency-Key header yes string Required Stripe-compatible idempotency key. Retries with the same key replay the original response for 24 hours and can never create a second billed/permanent seal. MUST match ^[A-Za-z0-9._-]{1,255}$; missing or malformed values return 400.

Request body (application/json, required)

Field Type Required Description
body string yes Plaintext qub body to seal. Capped at 51_200 bytes (50 KB) at the route layer to match the Arweave chunk-economic sweet spot. The CBOR-envelope layer carries a separate 102_400 (100 KB) cap that accommodates pact bodies arriving via /upload; the route-level cap is text-qub-specific.
unlock_at integer yes Unix timestamp (seconds) for when the qub should unlock. MUST be a finite integer strictly greater than now and no more than 10 years in the future. NaN, Infinity, and non-integer floats are rejected at the API edge (Andre 2026-05-22 Finding 1) — sending one returns 400 invalid_unlock_at.
wrapper_key_b64url string yes Required caller-generated base64url-no-pad encoding of a cryptographically random 32-byte AES-256-GCM outer-wrapper key K. Generate and retain K before calling /seal; the Worker uses it only in memory and never persists it. Idempotency replays deliberately redact K, so the retained caller copy is what makes a lost first response recoverable: append it as #<K> to the replayed fragment-less delivery URL. — minLength: 43, maxLength: 43, pattern: ^[A-Za-z0-9_-]{43}$
sender_label string no Optional display name for the sender. Subject to the same Unicode hygiene as title — NFC-normalised; hostile codepoints (bidi-override, ZWSP, tag-block, BOM, C0/C1) are rejected. Returns 400 invalid_sender_label on violation. — maxLength: 80
title string no Optional plaintext title surfaced on the viewer countdown before reveal (PROTOCOL.md §3.2 v1.0). 1..=100 NFC code points. Bound to qub_id via title_hash (PROTOCOL.md §4.1) so a gateway cannot swap the displayed title without invalidating qub identity. Empty strings are rejected — the canonical encoding of an absent title is field omission. Unicode hygiene (PR 0c, Andre Finding 15): rejects bidi overrides (U+202A-U+202E, U+2066-U+2069), ZWSP (U+200B), tag-block (U+E0000-U+E007F), BOM (U+FEFF), C0+DEL, and C1 controls. ZWJ / ZWNJ / variation selectors / LRM / RLM are KEPT because they're load-bearing for Devanagari / Arabic / emoji / RTL text. Returns 400 invalid_title. Content policy (PR 1d, Andre Finding 12): titles that combine urgency wording (verify, confirm, suspended, urgent, ...) with a reserved brand name (Chase, PayPal, Apple, ...) reject as invalid_title to defang brand-impersonation phishing through the share-preview countdown. — maxLength: 100

Responses

Status Description Body
200 qub sealed and uploaded. SealResponse (application/json)
400 Bad request. Error (application/json)
401 API key required. Error (application/json)
402 Payment required. code: QUOTA_EXHAUSTED when the per-key qubs_remaining is at 0 on a tier without metered overage; code: MONTHLY_CAP_REACHED when a Builder customer has opted into max_monthly_charge_usd and the projected overage would breach it. Error (application/json)
429 Rate limit or Arweave-cap reached. See the operation description for the canonical codes: RATE_LIMIT_API_KEY (per-key in-window), DAILY_KEY_LIMIT (per-key daily Arweave seal cap), DAILY_ACCOUNT_LIMIT (per-account aggregate daily Arweave seal cap). Daily-cap codes added by PR 1c + PR S6 (Andre 2026-05-22 review). Error (application/json)
500 Server error during sealing. Error (application/json)
502 Upstream permanent-storage failure. Error (application/json)
503 Storage-spend circuit breaker tripped (code: CIRCUIT_BREAKER); seals are temporarily refused operator-wide until the cap is lifted. Error (application/json)

200 response body — SealResponse

Field Type Required Description
tx_id string yes
delivery_url string (uri) yes Delivery URL with the OuterWrapper key embedded as the URL fragment (#<base64url(K)>, PROTOCOL.md §13.6). Sharing this URL hands the receiver everything needed to read the qub; truncating the fragment makes the qub unreadable.
qub_id string yes
unlock_at number yes
drand_round integer yes
short_delivery_url string (uri) no Full short-URL form https://qub.social/s/<code>#<key> when a 7-character base62 short code was allocated at seal time. Prefer this for share surfaces. Absent if allocation failed transiently — delivery_url is always usable as a fallback.
wrapper_key_b64url string yes Base64url-no-pad encoding of the 32-byte AES-256-GCM wrapper key K (PROTOCOL.md §13.6). Returned so callers that build their own URLs (e.g. for offline distribution) can compose the fragment themselves. The Worker never persists K — it lives only in this response. Identical to the value embedded in delivery_url.

Upload

Upload already-sealed qub bytes

POST /api/v1/upload — Upload already-sealed qub bytes

Auth: Turnstile or API key

Accepts already-sealed bytes: an OuterWrapper for private delivery or bare SealedQubCbor for public delivery. Before acknowledgement, the artifact and exact individual-transaction publication state are synchronously written to durable R2. The route then attempts a transparency-log append when LOG_DO is configured; only a successful append adds log_seq, receipt, and anchor_status to the response, while append failure is currently fail-soft. The pre-signed individual permanent-storage transaction is posted asynchronously. device_id remains the anonymous quota/entitlement locator. A valid matching browser session may select the linked-account quota/history branch; without a cookie the route stays anonymous and MUST NOT follow device_link. A presented mismatched cookie returns SESSION_DEVICE_MISMATCH; other presented invalid/expired/revoked/unavailable authority fails closed instead of silently downgrading. Browser clients also supply Turnstile; API-key callers use their separate Bearer authority. A valid Idempotency-Key is required. The 24-hour replay is semantic-operation-bound: retry-ephemeral wrapper bytes/nonces and Turnstile tokens do not create a second mutation, while incompatible reuse returns IDEMPOTENCY_KEY_CONFLICT. Missing keys return IDEMPOTENCY_KEY_REQUIRED; strong idempotency failure returns IDEMPOTENCY_UNAVAILABLE without uploading.

Parameters

Name In Required Type Description
Idempotency-Key header yes string Required stable operation key. Same-operation retries replay the original response and cannot consume quota or publish twice. MUST match ^[A-Za-z0-9._-]{1,255}$.

Request body (application/json, required)

Field Type Required Description
wrapped_cbor_base64 string yes Base64-encoded selected upload payload. Private delivery sends canonical OuterWrapper CBOR and keeps K in the client URL fragment; public delivery sends bare canonical SealedQubCbor and sets is_public: true. The field name is retained for API compatibility. The upload route treats either shape as opaque bytes.
device_id string yes Device identifier (32 lowercase hex chars). Used as the KV key, rate-limit bucket, and sealed-history shard. Pattern enforced since PR S4 (Andre 2026-05-22 SYSTEMIC-THREAT-REVIEW Finding 43); previously any-string was accepted. — pattern: ^[a-f0-9]{32}$
turnstile_token string no Cloudflare Turnstile token. Optional when using API key auth.
content_size integer yes Size in bytes of the selected upload payload, not the plaintext body. For private delivery this is the OuterWrapper size, including AEAD and CBOR framing overhead; for public delivery it is the bare SealedQubCbor size.
intent enum ("announcement" / "thesis" / "prediction" / "letter" / "secret" / "commitment" / "proof" / "verdict") no Optional compose intent. When present and on the allowlist, the Worker attaches it as the Intent storage tag for the viewer's ?from={intent} viral-loop CTA. Unknown values are silently dropped.
title string no Optional display-only title for the creator's sealed-history folder lists (/messages, /history), persisted on the per-device sealed-history index so titles surface cross-device. NFC-normalised and capped at 100 code points (matching the compose-side SealedQub::title bound), validated at the edge via normaliseAndValidateText. A hostile or over-long title is silently dropped to absent rather than failing the upload — the qub is already sealed and the canonical title rides inside the selected upload payload. The Worker never renders this declaration to viewers; it is creator-private folder metadata.
unlock_at integer yes Required Unix-seconds reveal time declared by the client. The upload route deliberately treats both accepted byte shapes as opaque and uses this value only for metadata, sealed-history backup, lifecycle scheduling, and the asserted transparency-log leaf. The cryptographic artifact remains governed by the inner SealedQub and drand regardless of this declaration.
outcome_at integer no Optional outcome time (Unix seconds UTC) on a verdict-bearing qub (verdict-uplift-plan §3.1). Mirrors SealRequest.outcome_at; validated via the same shape rules (integer, positive, >= unlock_at, <= now + 10 years). The value also rides inside the selected upload payload — this surface is an out-of-band declaration so the byte-blind route can schedule the verdict-CTA email (V1.6). Persisted on the creator-lifecycle record (cl:<tx_id>).
qub_id_hex string yes Required 64-char lowercase hex declaration of the inner SealedQub's qub_id. The byte-blind upload route uses it as metadata and in the asserted transparency-log leaf; verifiers establish the actual qub_id from the decrypted protocol fields. For private delivery it is also the OuterWrapper AEAD AAD. — pattern: ^[0-9a-f]{64}$
author_fingerprint string no Optional 64-char lowercase hex of the creator's pubkey fingerprint (PROTOCOL.md §2.2). Privacy-by-default — opt-in per qub. When present and well-formed, attached as an Author storage tag so the viewer countdown can surface 'Sealed by @handle' after attestation lookup. Published only with a valid author_pubkey + author_pop_sig proof of possession (QUB-UPLOAD-001): the byte-blind Worker cannot verify the in-envelope signature, so without the detached proof the fingerprint is dropped — a direct API call cannot spoof another creator's pre-reveal byline. When omitted, no Author tag is written and the qub is unattributed. Invalid values are silently dropped. — pattern: ^[0-9a-f]{64}$
author_pubkey string no Optional base64url-no-pad of the creator's 1952-byte ML-DSA-65 public key. Paired with author_pop_sig to prove possession of the key behind author_fingerprint (QUB-UPLOAD-001). The Worker requires SHA3-256(pubkey) to equal author_fingerprint and the signature to verify before publishing the Author tag. Send together with author_fingerprint. — pattern: ^[A-Za-z0-9_-]+$
author_pop_sig string no Optional base64url-no-pad of the ML-DSA-65 signature over the upload-author proof-of-possession challenge "QUB_UPLOAD_AUTHOR_V1" || author_fingerprint(32) || chash(32), where chash = SHA3-256(selected upload payload bytes). Binds the proof to this specific upload so it can't be replayed onto another. — pattern: ^[A-Za-z0-9_-]+$
parent_tx_id string no Optional parent qub storage tx_id (43-char base64url) for reply-chain qubs (FUTURE.md §11.1). When present and well-formed, attached as a Parent-Tx-Id storage tag so the viewer reveal page can render a 'Replied to {parent}' back-link without a server-side reverse index. Invalid values are silently dropped. — pattern: ^[A-Za-z0-9_-]{43}$
verdict_outcome integer no Optional verdict outcome enum (1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable). Set ONLY when uploading a verdict qub (intent=verdict + valid parent_tx_id); ignored otherwise. The authoritative outcome rides inside the selected upload payload (opaque to this route), while this enum populates the verdicts_for_parent:<parent>:<tx> discovery index so the parent's reveal page can render the §5.3 Published-state inline label without decoding the verdict artifact. Values outside 1..=4 are silently dropped (verdict-uplift-plan §5.3, V1.4b).
parent_intent enum ("prediction" / "commitment" / "announcement" / "thesis") no Optional parent qub intent — set ONLY when uploading a verdict qub. Lets the V1.7 subscriber-rendered fan-out cron pick the per-intent email template without a round-trip back to permanent storage for the parent's Intent tag. The Worker validates against the four verdict-bearing intents; unknown values are silently dropped and the cron skips the fan-out rather than guessing a template (verdict-uplift-plan §7.3 Path B, V1.7).
verdict_proof_pubkey string no Optional base64url-no-pad of the verdict author's 1952-byte ML-DSA-65 public key. Paired with verdict_proof_sig to prove the uploader is the PARENT author before the Worker writes the authoritative verdicts_for_parent index + subscriber fan-out (QUB-UPLOAD-002). The Worker requires SHA3-256(pubkey) to equal the parent's published Author fingerprint. Set ONLY when uploading a verdict qub. — pattern: ^[A-Za-z0-9_-]+$
verdict_proof_sig string no Optional base64url-no-pad of the ML-DSA-65 signature over the verdict-parent proof-of-possession challenge "QUB_VERDICT_PARENT_V1" || parent_tx_id(43) || outcome(1) || verdict_qub_id(32). Binds the proof to this verdict + outcome so it can't be lifted onto another. Absent/invalid ⇒ the verdict is still durably accepted and scheduled for permanent storage but gets no authoritative parent-verdict state (fail-soft). — pattern: ^[A-Za-z0-9_-]+$
creator_email string (email) no Optional creator email for lifecycle emails (DISTRIBUTION-STRATEGY §12.1 / TODOS P0-10). Forwarded only when creator_lifecycle_opt_in is true. The Worker writes a cl:<tx_id> KV record and fires seal_confirmation immediately. Validated for email-like shape; invalid values cause the opt-in to be silently dropped (the upload still succeeds).
creator_locale string no Optional BCP 47 locale tag for lifecycle emails. Defaults to en when absent. Persisted with the creator-lifecycle record so deferred sends (pre-reveal reminder, reveal notification, watcher milestone, anniversary) land in the right language.
creator_lifecycle_opt_in boolean no Lifecycle opt-in flag. Must be true to enable — anything else (missing, false, truthy non-boolean) is treated as opt-out and the lifecycle email surface is skipped.
creator_reveal_date_short string no Optional pre-formatted short reveal date (matches the viewer's format_ts_short). Surfaced verbatim in the seal_confirmation email body when lifecycle opt-in is active.
wrapper_key_b64url string no Optional base64url-no-pad encoding of the 32-byte AES-256 wrapper key K (PROTOCOL.md §13.6). Engages the W13 recovery channel ONLY when the client also sets creator_lifecycle_opt_in: true AND creator_email matches the device's verified (sybil-linked) email. On the gated path the Worker composes ${origin}/c/${tx_id}#${K}, uses it as {link} in the seal_confirmation email, and stores it on the per-identity sealed-history entry as a recovery anchor. Outside the gate, K stays in the browser; pass it only when the user has explicitly opted into the recovery channel. Malformed values are silently dropped (the lifecycle email falls back to the legacy fragment-less URL). Privacy trade-off documented in locales/en/privacy.md §2.3. — pattern: ^[A-Za-z0-9_-]{43}$
is_public boolean no Optional public-qub flag (delivery-layer visibility, default false). When true the caller has uploaded the raw SealedQubCbor with NO AES-256-GCM outer wrapper (PROTOCOL.md §13.8) — tlock-only, like a pact — so the tx_id alone decrypts after unlock and wrapped_cbor_base64 carries no fragment-keyed wrapper. The Worker stamps a Visibility: public storage tag and emits fragment-less working delivery links (sealed-history delivery_url, seal_confirmation email, reveal notifications). Absent / false keeps the wrapped, fragment-gated model. Strict-true only; any other value reads as private.

Responses

Status Description Body
200 Upload successful. UploadResponse (application/json)
400 Bad request. Error (application/json)
402 Payment required — entitlement exhausted. Error (application/json)
403 Turnstile verification failed. Error (application/json)
413 Payload too large for the device's entitlement tier. Error (application/json)
429 Rate limited. Error (application/json)
502 Upstream permanent-storage failure. Error (application/json)

200 response body — UploadResponse

Field Type Required Description
tx_id string yes Storage transaction ID.
delivery_url string (uri) yes Fragment-less canonical URL <origin>/c/<tx_id>. For public delivery (is_public: true) this is the complete working URL. For private delivery the Worker never receives K on the default upload path, so callers MUST append #<base64url(K)> themselves; without that fragment the private qub is unreadable. Server-side /seal returns its complete fragment-bearing URL directly.
short_code string no 7-character base62 short code mapped to tx_id in KV. Resolves via /s/{code} to the canonical /c/{tx_id} viewer page. Absent when the allocator failed transiently — delivery_url is always usable as a fallback.
qubs_remaining integer no Post-decrement free-tier quota for the verified identity that just sealed, after this upload counted against the shared sybil-linked counter. Present only for free-tier browser uploads made by a verified identity. Absent for creator-tier (quota lives on the entitlement record), API-key, and anonymous uploads. Clients use it to keep their local identity cache in sync without an extra round-trip to GET /api/v1/identity.
log_seq integer no Optional zero-based transparency-log leaf position. Present only as part of the all-or-nothing log tuple when the inline LogDO append succeeded. Absence means the publication is durable but makes no transparency-log acceptance claim.
anchor_status enum ("pending") no Present only with log_seq and receipt; means the accepted leaf is waiting for a covering anchor. It does not mean an Arweave anchor already exists.
receipt object no Receipt returned after a successful log append. The signature is deployment-gated: sig_b64url is the empty string when RECEIPT_SK is absent or invalid, and such a receipt is not signed/non-repudiable. The current compiled LogProfile.receipt_pubkey is also empty until a verifier release pins the provisioned public key.

POST /api/v1/upload-auth — Pre-check upload eligibility

Auth: Turnstile or API key

Validates the device's entitlement tier and rate limits before uploading. Rate limited to 20 requests per 10 minutes per IP and 10 per 10 minutes per device.

Request body (application/json, required)

Field Type Required Description
turnstile_token string no Cloudflare Turnstile token. Optional when using API key auth.
device_id string yes
content_size integer yes Size in bytes of the sealed content.

Responses

Status Description Body
200 Eligibility check result. UploadAuthResponse (application/json)
400 Bad request. Error (application/json)
403 Turnstile verification failed. Error (application/json)
429 Rate limited. Error (application/json)

200 response body — UploadAuthResponse

Field Type Required Description
allowed boolean yes
tier string yes
max_size integer yes
reason string no

Engagement

Watch / view counters and notify-me subscriptions

POST /api/v1/qub/{tx_id}/notify — Subscribe an email to a qub's reveal

Auth: public

Adds an email address to the notify-me list for a sealed qub. The reveal-time cron sends a one-shot email when the qub unlocks. Always returns { ok: true } on success — re-submitting the same address is idempotent and updates the stored locale. Hard-capped at 1000 subscribers per qub. Rate limited 5 requests per minute per IP.

Parameters

Name In Required Type Description
tx_id path yes string

Request body (application/json, required)

Field Type Required Description
email string (email) yes Email address to subscribe. Max 200 chars; the domain is lowercased on store.
unlock_at integer no Unix seconds when the qub unlocks. Strongly preferred — without it the cron has no way to know when to send the email.
locale string no BCP 47 locale tag (e.g. en, pt-BR). Picks the language for the reveal email. Falls back to en if missing or unrecognised.
intent enum ("prediction" / "letter" / "secret" / "announcement" / "thesis" / "commitment" / "proof" / "verdict") no Compose intent of the subscribed qub. Captured at subscribe time so the reveal email picks an intent-aware subject + body variant where one exists (The prediction reveals now. etc). Optional — omitted, unknown, or per-intent-template-less values fall back to the generic notify template. The server validates against the allowlist and silently drops anything else.

Responses

Status Description Body
200 Subscribed (or cap reached — the cap is silent so the user doesn't see a degraded experience). OkResponse (application/json)
400 Invalid tx_id, email, or unlock_at. Error (application/json)
429 Rate limited. Error (application/json)

200 response body — OkResponse

Field Type Required Description
ok boolean yes

POST /api/v1/qub/{tx_id}/react — Record a reaction on a revealed qub

Auth: public

Records a called_it or wrong reaction for a revealed prediction and returns the updated tallies. Client-side dedup via localStorage per (tx_id, device) is assumed; ~10% inflation is acceptable. Rate-limited 10/min/IP as a shallow abuse floor — over-limit requests return current tallies without mutating.

Parameters

Name In Required Type Description
tx_id path yes string

Request body (application/json, required)

Field Type Required Description
reaction enum ("called_it" / "wrong") yes

Responses

Status Description Body
200 Updated reaction tallies. object (application/json)
400 Invalid reaction or tx_id. Error (application/json)

200 response body — inline

Field Type Required Description
called_it integer yes
wrong integer yes
total integer yes

GET /api/v1/qub/{tx_id}/verdict-watch — Read the current verdict-watcher count

Auth: public

Returns the current verdict-watcher count without modifying the set. Used by the reveal-page 30-second poll (plan §5.1) so the counter live-updates as new committers tap the CTA.

Parameters

Name In Required Type Description
tx_id path yes string

Responses

Status Description Body
200 Current exact count (cosmetic rounding for display happens client-side). object (application/json)
400 Invalid tx_id. Error (application/json)

200 response body — inline

Field Type Required Description
count integer yes

POST /api/v1/qub/{tx_id}/verdict-watch — Commit to watching for a verdict (per-device dedupe)

Auth: public

Increments the per-qub verdict-watcher set with the caller's device_id. Unlike /watch, this counter is per-device-deduped (verdict-uplift-plan §5.1.1): each device counts at most once per qub; re-click is a no-op; unsubscribe does NOT decrement (decoupling subscribed-now from historically-committed defeats hit-and-run gaming). The notify flag that drives verdict-time email fan-out is independent and lives on the NotifySubscriber record. Returns the new exact count plus a watching flag the client uses to flip the CTA into 'you're already watching' state.

Parameters

Name In Required Type Description
tx_id path yes string

Request body (application/json, required)

Field Type Required Description
device_id string yes 32-char lowercase hex device identifier. Same device_id shape used by /upload and /checkout. — pattern: ^[a-f0-9]{32}$

Responses

Status Description Body
200 New count after this commit (rate-limited responses return { count: 0, watching: false }). object (application/json)
400 Invalid tx_id, body, or device_id. Error (application/json)

200 response body — inline

Field Type Required Description
count integer yes
watching boolean yes True iff this device's id is in the set after the call. Used by the client to flip the CTA between 'commit' and 'you're already watching'.

POST /api/v1/qub/{tx_id}/view — Increment the post-reveal view counter

Auth: public

Increments and returns the post-reveal view count. Same shape as the watch counter but tracked on a separate KV key. Displayed on the revealed screen as 'Seen by {N} people'. Rate limited 10 requests per minute per IP.

Parameters

Name In Required Type Description
tx_id path yes string

Responses

Status Description Body
200 New count after this increment (or 0 if rate-limited). EngagementCountResponse (application/json)
400 Invalid tx_id. Error (application/json)

200 response body — EngagementCountResponse

Field Type Required Description
count integer yes Current count after this increment. Returns 0 if rate-limited (so the client can render zero or its cached value rather than an error).

POST /api/v1/qub/{tx_id}/watch — Increment the pre-reveal watch counter

Auth: public

Increments and returns the 'watching' count for a sealed qub. Used by the viewer countdown screen as social proof. No per-fingerprint dedup — accepts ~10% inflation in exchange for a much simpler implementation. Rate limited 10 requests per minute per IP. On rate-limit returns { count: 0 } (200) so the client renders zero or its cached value.

Parameters

Name In Required Type Description
tx_id path yes string

Responses

Status Description Body
200 New count after this increment (or 0 if rate-limited). EngagementCountResponse (application/json)
400 Invalid tx_id. Error (application/json)

200 response body — EngagementCountResponse

Field Type Required Description
count integer yes Current count after this increment. Returns 0 if rate-limited (so the client can render zero or its cached value rather than an error).

API Keys

API key checkout, retrieval, and rotation

POST /api/v1/api-keys/rotate — Rotate the current API key

Auth: API key

Issues a new API key for the authenticated client and stores a grace-period mapping so the old key continues to work for one hour. Per-period quota state and ownership indexes move forward; created_at is preserved. No scope gate — successfully presenting the current key already proves ownership. Concurrent lifecycle mutations are blocked via a two-minute owner-token lease backed by the QuotaDO. Idempotency-Key is mandatory. The newly minted secret appears only in the live first response and is never persisted in the replay store. A same-key replay returns 409 with the ROTATION_REPLAYED redacted sentinel; use the still-valid old key during its grace window to rotate again with a fresh idempotency key if the first response was lost.

Parameters

Name In Required Type Description
Idempotency-Key header yes string Required Stripe-compatible idempotency key. The first response carries the secret; a replay within 24 hours is deliberately redacted and returns 409 ROTATION_REPLAYED. MUST match ^[A-Za-z0-9._-]{1,255}$; missing or malformed values return 400.

Responses

Status Description Body
200 Rotation succeeded; the response carries the new raw key. ApiKeyRotateResponse (application/json)
400 Missing or malformed Idempotency-Key. Error (application/json)
401 API key required or invalid. Error (application/json)
403 Disabled key or IP not allowed. Returns { code: 'API_KEY_DISABLED' | 'IP_NOT_ALLOWED' }. Error (application/json)
409 A rotation is in progress/committed, or an idempotent replay was redacted as ROTATION_REPLAYED. Error (application/json)
429 Per-account rotation limit exceeded (10 per hour). Error (application/json)
503 Strong idempotency, quota, account authority, usage migration, or rotation backend unavailable; retry safely with the same idempotency key. Error (application/json)

200 response body — ApiKeyRotateResponse

Field Type Required Description
key string yes The new raw qub_sk_... API key. The old key continues to work through the one-hour grace mapping (or until a newer rotation advances it).

Payment

Stripe subscriptions plus the account-bound, prepaid Lightning Builder adapter

GET /api/v1/entitlements — Check device entitlement tier

Auth: public

Returns the current entitlement tier and remaining qub count for a device.

Parameters

Name In Required Type Description
device_id query yes string Device identifier.

Responses

Status Description Body
200 Entitlement info. EntitlementsResponse (application/json)
400 Missing device_id. Error (application/json)

200 response body — EntitlementsResponse

Field Type Required Description
tier enum ("free" / "pro") yes
qubs_remaining integer yes
qubs_allowance integer no Per-period allowance of the highest active Pro source (500 monthly / 6,000 annual). Absent for free tier or when no source is active.
purchased_at integer (int64) no Unix epoch seconds at which the entitlement was first purchased (sourced from the rail's checkout / receipt timestamp). Absent on the free-tier response.
current_period_end integer (int64) no Unix epoch seconds when the current paid period ends. Derived from max(period_end) across active sources (PAYMENTS.md v1.0 §4.4). Absent for Free customers and for Pro customers before the first renewal lands.
subscription_status enum ("active" / "past_due" / "cancelled") no Highest-level status across active payment sources (PAYMENTS.md v1.0 §4.4). Absent when the entitlement has no payment sources.
sources_summary array of object no Per-rail summary so the client can render the right cancellation surface (PAYMENTS.md §11.2 / §14). Provider-agnostic — never includes per-rail customer / subscription / transaction IDs.

Webhooks

Webhook registration for qub unlock notifications

GET /api/v1/webhooks — List webhooks

Auth: API key

List all webhooks registered by the authenticated API key.

Responses

Status Description Body
200 List of webhooks. array of Webhook (application/json)
401 API key required. Error (application/json)

POST /api/v1/webhooks — Register a webhook

Auth: API key

Register a webhook URL to be notified when a specific qub unlocks. Requires an API key with the webhooks scope. The Worker performs a one-time URL ownership challenge (POSTs a verification token to the candidate URL and expects an echo) before the webhook is recorded; failed verification returns 422. Each API key has a webhook_limit (Builder default 25); attempting to register beyond the limit returns 403. Rate limited to 10 registrations per API key per minute. The shared secret is used to sign every delivery as Qub-Signature: t=<unix>,v1=<hex> where <hex> is HMAC_SHA256(secret, '<unix>.<rawBody>'). Receivers MUST verify the HMAC over the timestamp-prefixed payload and reject deliveries where now - t > 300 (5-minute replay window). Every delivery payload carries a stable delivery_id (deterministic per webhook + qub + event) — receivers SHOULD dedupe on it, because retries follow a backoff ladder (1m/5m/15m/1h/6h/24h then daily, up to 12 attempts across the 7-day retention window) and overlapping sends can occasionally duplicate.

Request body (application/json, required)

Field Type Required Description
tx_id string yes Storage transaction ID to watch.
url string (uri) yes URL to receive the webhook POST.
secret string yes Shared secret used for HMAC-SHA256 signature verification. The Worker sends the signature as Qub-Signature: t=<unix>,v1=<hex> where <hex> is HMAC_SHA256(secret, '<unix>.<rawBody>'). Receivers MUST verify the HMAC over the timestamp-prefixed payload and reject deliveries where now - t > 300 (5-minute replay window). The v1= algorithm tag is forward-compatible — future rotations may add v2= alongside v1= during a deprecation window. Minimum 16 characters. — minLength: 16

Responses

Status Description Body
201 Webhook registered. WebhookCreateResponse (application/json)
400 Bad request. Error (application/json)
401 API key required. Error (application/json)
403 Per-key webhook limit reached (webhook_limit on the key record). Error (application/json)
422 Webhook URL ownership verification failed (no echo of the verification token). Error (application/json)
429 Per-key registration rate limit exceeded. Error (application/json)

201 response body — WebhookCreateResponse

Field Type Required Description
webhook_id string yes

DELETE /api/v1/webhooks/{id} — Remove a webhook

Auth: API key

Delete a previously registered webhook.

Parameters

Name In Required Type Description
id path yes string Webhook ID.

Responses

Status Description Body
204 Webhook deleted (no response body). —
401 API key required. Error (application/json)
404 Webhook not found. Error (application/json)

Viewer

Public viewer page (browsers + bots)

GET /api/v1/og/{tx_id}.png — Dynamic Open Graph image

Auth: public

Dynamic OG card for a qub. Sealed state renders intent + reveal date + watching count; revealed state renders intent + called-it percentage as a results card. R2 cache-aside by state + watching/reaction bucket so unfurls hit cache for the typical viewer flow. Note: despite the .png URL suffix (kept for compatibility with OG cards already in the wild), the response body is SVG (Content-Type: image/svg+xml). Major social-media unfurlers (X, Discord, Slack, Reddit, Threads, Bluesky) accept SVG.

Parameters

Name In Required Type Description
tx_id path yes string

Responses

Status Description Body
200 SVG OG card (1200×630 viewport). string (binary) (image/svg+xml)
404 Unknown tx_id. Error (application/json)

Meta

API metadata

GET /api/v1/log/consistency — Transparency-log consistency proof

Auth: public (optional API key)

Returns the canonical-CBOR RFC 9162 consistency proof showing the log at tree size first is an append-only prefix of the log at size second (PROTOCOL.md §16.9): the two committed roots, the consistency node set, and both AnchorRefs. v1 serves sizes up to 512 — the far-more-frequent inclusion proof is already O(log N), so coordinate-walk consistency serving is deferred per §16.7. Cached public, max-age=3600.

Parameters

Name In Required Type Description
first query yes integer First (smaller) anchored tree size. 1 <= first <= second <= 512.
second query yes integer Second (larger) anchored tree size. first <= second <= 512.

Responses

Status Description Body
200 Canonical-CBOR ConsistencyProof (first/second sizes, both roots, consistency node set, both AnchorRefs). string (binary) (application/cbor)
400 Missing or non-canonical sizes, first > second, or second > 512. Error (application/json)
404 One or both sizes have not been anchored yet. Error (application/json)
429 Rate limited. Error (application/json)
503 Transparency-log store unbound or audit substrate incomplete — retry. Error (application/json)

GET /api/v1/openapi.json — OpenAPI specification

Auth: public

Returns this OpenAPI 3.1.0 specification document.

Responses

Status Description Body
200 OpenAPI specification. object (application/json)

Embed

F4 embed loader + iframe shell + bundled iframe app for third-party page embedding

GET /oembed — oEmbed discovery

Auth: public

oEmbed 1.0 discovery endpoint for public qubs. Returns a JSON oEmbed payload whose <qub-embed src=...> uses the fragment-less public viewer URL, so WordPress, Notion, Medium, and Substack can auto-embed without a manual snippet. Private delivery requires the complete secret-bearing URL and should use the explicit publisher snippet instead; this endpoint never reflects a wrapper key.

Parameters

Name In Required Type Description
url query yes string (uri) Canonical qub viewer URL (e.g. https://qub.social/c/<tx_id>).
format query no enum ("json") Response format. Only json is supported.
maxwidth query no integer
maxheight query no integer

Responses

Status Description Body
200 oEmbed JSON. object (application/json)
404 URL does not match a known qub. Error (application/json)

Identity

GET /api/v1/handle/{handle} — Resolve a qub handle to a representative linked fingerprint and public attestation

Auth: public

Public reverse lookup. Every email-verified user has a handle (auto-allocated or personalised, IDENTITY.md §3.2.5). The handle belongs to the email-scoped logical identity; this response selects one currently linked fingerprint as a projection locator, not as proof that one keypair owns the handle. Returns 410 with a released_shape discriminator while a recently-released handle is still in its cooldown window.

Parameters

Name In Required Type Description
handle path yes string Normalised handle (no leading @).

Responses

Status Description Body
200 Handle is claimed; body contains a representative linked fingerprint and the projected public attestation record. HandleLookupResponse (application/json)
400 invalid_handle — the handle fails normalisation (charset, length, or reserved). Error (application/json)
404 Returned in two cases, both with { error: "not_found" }: (a) the handle has never been claimed; or (b) its logical identity has no linked attestation available for public projection (or a legacy fingerprint-owned entry is orphaned). Error (application/json)
410 Handle was recently released and is in cooldown. released_shape is "auto" (1h cooldown) or "user" (30d cooldown). object (application/json)

200 response body — HandleLookupResponse

Field Type Required Description
fingerprint string yes Representative linked pubkey fingerprint selected to project the public attestation response. The handle belongs to the email-scoped logical identity, not to this individual key. — pattern: ^[0-9a-f]{64}$
attestations AttestationRecord yes

GET /api/v1/identity/attestation/{fingerprint} — Look up public attestations for a pubkey fingerprint

Auth: public

Unauthenticated public lookup. Returns the attestation record for a given author-key fingerprint (IDENTITY.md §2 + §4), or 404 if the keypair is unattested (Layer 0). Callers that model Layer 0 as a normal state may set allow_missing=true to receive a discriminated 200 result (found: true plus the record, or { found: false }). If allow_missing is present it must occur exactly once as lowercase true or false. The viewer uses this at reveal time to render the richest identity label per §5.3. Found records are cached at the edge for 5 minutes; explicit misses are not stored.

Parameters

Name In Required Type Description
fingerprint path yes string 64-char lower-case hex SHA3-256 of the author public key.
allow_missing query no boolean When true, return an explicit successful miss instead of 404 for an unattested fingerprint.

Responses

Status Description Body
200 Attestation record, or a discriminated found/missing result when allow_missing=true. AttestationRecord or AttestationLookupMiss (application/json)
400 Malformed fingerprint or invalid/duplicated allow_missing selector. Error (application/json)
404 No attestations for this fingerprint when allow_missing is omitted or false (Layer 0 / not attested). Error (application/json)

200 response body — AttestationRecord

Field Type Required Description
found const true no Present as true on a found public lookup when allow_missing=true; omitted from the legacy lookup and mutation responses.
attestations array of AttestationEntry yes
pubkey_alg integer yes sig_alg registry value (PROTOCOL.md §9.2). Phase 2 always 1 (ML-DSA-65).
created_at integer yes
updated_at integer yes

200 response body — AttestationLookupMiss

Field Type Required Description
found const false yes

Pact

POST /api/v1/pact/cosign/{staging_id} — Co-sign and publish a staged pact

Auth: Turnstile

Browser ceremony in which Party B submits their ML-DSA-65 cosigner signature over the same sig_input as Party A. The worker merges both signatures into the envelope, seals with tlock, durably stores the artifact, and schedules permanent-storage publication. Pact publication does not currently append to the transparency log. If Party B's contact in the staged pact is an email, the cosigner is gated by a short-lived 15-minute email-verification marker produced by /api/v1/auth/verify (protocol spec §9.7). When the magic-link request that produced the marker carried a cosigner_fingerprint (PC-3 binding), the submitted cosigner_pubkey must hash (SHA3-256) to the same fingerprint — otherwise the cosign is rejected. Turnstile is required; API-key bearer authentication is not accepted.

Parameters

Name In Required Type Description
staging_id path yes string

Request body (application/json, required)

Field Type Required Description
cosigner_pubkey string yes
cosigner_signature string yes
turnstile_token string yes

Responses

Status Description Body
200 Pact sealed. object (application/json)
400 Invalid signature, pubkey, or email-binding marker missing. Error (application/json)
404 Staging id not found or expired. Error (application/json)
409 Pact is already cosigned (already_cosigned) or another cosign is in progress (cosign_in_progress). Retry of the in-progress case is safe once the prior request completes. Error (application/json)
502 Durable publication failed before acknowledgement. Error (application/json)

200 response body — inline

Field Type Required Description
tx_id string yes
delivery_url string yes

POST /api/v1/pact/invite/accept — One-click accept of a pact invite from the email link

Auth: public

Consumes the single-use emailed pact capability and writes a short-lived, pact-scoped cosign marker. Invite acceptance does not sign the browser into the invite address, issue a browser session, mutate an identity, or create an attestation. An optional cosigner fingerprint preserves PC-3 key binding.

Request body (application/json, required)

Field Type Required Description
staging_id string yes Staging ID from the URL path. — pattern: ^[a-f0-9]{32}$
token string yes HMAC-signed invite token from the ?t= query param of the email link. Format: base64url(payload).base64url(sig).
cosigner_fingerprint string no Optional — SHA3-256 fingerprint of the keypair Party B will cosign with. When present, the Worker pins the marker to this fingerprint so a different keypair cannot later cosign (PC-3 binding). — pattern: ^[a-f0-9]{64}$

Responses

Status Description Body
200 Pact-scoped cosign marker committed. No account session or identity mutation is performed. object (application/json)
400 Invalid token (TOKEN_INVALID), expired (TOKEN_EXPIRED), staging_id mismatch, malformed body, or invalid fingerprint. Error (application/json)
404 The staging record was retracted or has expired. Error (application/json)
409 Pact has already been cosigned (already_sealed). Error (application/json)
410 Token already used (TOKEN_USED). Single-use enforcement. Error (application/json)
503 Auth/token authority unavailable (AUTH_DISABLED or service_unavailable). Error (application/json)

200 response body — inline

Field Type Required Description
ok const true yes

POST /api/v1/pact/stage — Stage a signed pact envelope for co-signing

Auth: Turnstile

Browser ceremony that stages a signed pact envelope. device_id remains a quota locator. A valid matching browser session may select the account quota/index branch; no cookie stays anonymous and never follows device_link. Presented bad/unavailable session authority fails closed and a mismatch returns SESSION_DEVICE_MISMATCH. A valid Idempotency-Key is mandatory so an ambiguous response cannot charge or create a second staging artifact; missing keys return IDEMPOTENCY_KEY_REQUIRED. Turnstile, signature proof, recipient send budget, and the seven-day stage lifetime remain independent controls. API-key bearer authentication is not accepted.

Parameters

Name In Required Type Description
Idempotency-Key header yes string Required stable operation key for the signed staging mutation. MUST match ^[A-Za-z0-9._-]{1,255}$.

Request body (application/json, required)

Field Type Required Description
envelope_cbor string yes Base64-encoded canonical CBOR QubEnvelope with content_type=0x03, sig_alg=0x01, author_pubkey and author_signature present, cosigner fields absent.
turnstile_token string yes
device_id string yes
locale string no Optional BCP 47 locale used for the review invite email.

Responses

Status Description Body
201 Pact staged. object (application/json)
400 Invalid request. Error (application/json)
403 Turnstile verification failed. Error (application/json)
429 Rate limited. —

201 response body — inline

Field Type Required Description
staging_id string yes
staging_url string yes
email_sent boolean yes Whether the invite email to Party B was accepted for delivery.
email_skipped_reason string or null yes Why no invite was sent when email_sent is false; null when it was sent. unverified_author: the signing key carries no email attestation matching the browser session — attest it (attestEmailViaSession) and call sendPactInvite.

DELETE /api/v1/pact/stage/{staging_id} — Retract a staged pact before co-signing

Auth: Turnstile

Party A proves possession of the author signing key and retracts a pending staged pact. Requires a fresh retract signature over SHA3-256("QUB_PACT_RETRACT_V1" || staging_id_bytes).

Parameters

Name In Required Type Description
staging_id path yes string

Request body (application/json, required)

Field Type Required Description
retract_signature string yes Base64 retract signature from Party A's signing key.
turnstile_token string yes

Responses

Status Description Body
200 Retracted. —
400 Invalid request or signature. Error (application/json)
404 Staging id not found or already sealed. Error (application/json)

GET /api/v1/pact/stage/{staging_id} — Review a staged pact

Auth: public

Returns the staged pact's CBOR envelope fields, decoded PactTerms, and Party A's public key so Party B can review before co-signing. If the pact has already been co-signed, returns a sealed redirect payload with tx_id and delivery_url. While awaiting co-signature the payload also carries invite_emailed (boolean): whether the invite email to Party B has been accepted for delivery. The sealed payload carries unlock_at (Unix seconds, or null once the staging object has aged out): a pact co-signed inside its unlock buffer is sealed but not yet openable, and clients should hold until then rather than forward to the viewer countdown.

Parameters

Name In Required Type Description
staging_id path yes string

Responses

Status Description Body
200 Staged pact details (or sealed redirect). object (application/json)
404 Staging id not found or expired. Error (application/json)

This page is generated from workers/api/openapi.json by workers/api/scripts/build-openapi-md.mjs. For the raw JSON, see /api/v1/openapi.json.