Authentication
Every documented developer API call carries a secret Bearer token. Secret keys are minted in your console and shown once at creation. Store them in your backend secret manager, never in client code. The key dialog defaults to this server-safe key type.
curl https://proseid.com/api/v1/records \ -H "Authorization: Bearer proseid_sk_..." \ -H "Content-Type: application/json"
Missing, invalid, or revoked tokens return 401 Unauthorized. A valid token that lacks the endpoint's validate, records, proof, schemas, or flows scope returns 403 Forbidden. Rate-limited requests return 429 with a Retry-After header.
{
"error": {
"code": "validation_failed",
"message": "the document did not pass validation",
"details": { ... }
}
}Idempotency
Pass an Idempotency-Key header when creating a record, schema, or Flow to make network retries safe. The same key and same request body returns the original resource; the same key with different content returns 409 idempotency_conflict.
curl https://proseid.com/api/v1/records \ -H "Authorization: Bearer proseid_sk_..." \ -H "Idempotency-Key: order_8f3a9b2c" \ -H "Content-Type: application/json" \ -d '{"flow":"acme/onboarding","responses":{"full_name":"Dana Lee"}}'
- → Use a unique key per logical operation (order ID, claim ID, etc.) — not per HTTP retry.
- → Same key + different body returns 409 Conflict.
- → Keys are scoped to your API key. Two separate keys can use the same Idempotency-Key independently.
Create, prepare, then publish
Management calls use a secret key with the schemas scope. They follow the same lifecycle as the dashboard: creating or replacing a working body does not make it runnable. You must prepare the exact saved body with a plain-language change message, then explicitly publish that prepared version as private or public.
The API key decides the organization; an orgId is never trusted from the request. A new schema begins only as a working version. Publication requires an explicit visibility value. Public schemas also require a verified email, a publisher handle, and a logical schema.schema_id beginning with that handle. A public schema can never become private again.
POST /api/v1/schemas creates the working schema and returns its internal schema_id.
GET /api/v1/schemas/{schema_id}/draft reads the complete working body and its concurrency hash.
PUT /api/v1/schemas/{schema_id}/draft replaces that complete working body.
POST /api/v1/schemas/{schema_id}/prepare freezes the saved body with a required change message.
POST /api/v1/schemas/{schema_id}/publish publishes the prepared version as explicitly private or public.
POST /api/v1/schemas/{schema_id}/drafts starts the next working version from the latest immutable release.
POST /api/v1/schemas Idempotency-Key: customer-eu-policy-v1 { "schema": { "protocol": "Proseid_v1.0", "schema_id": "acme/can-i-use-ai-eu", "version": "1.0.0", "metadata": { ... }, "definitions": { ... }, "state_model": { ... }, "logic_tree": [ ... ] } } → { "schema_id": "schema-document-id", "id": "schema-document-id", "version": "1.0.0", "status": "working_draft", "duplicate": false }
GET /api/v1/schemas/{id}/draft → { "schema": { ... }, "body_hash": "current hash", "prepared_id": null } PUT /api/v1/schemas/{id}/draft { "schema": { ...complete replacement body... }, "expected_body_hash": "optional hash returned by GET or the previous save" } → { "schema_id": "schema-document-id", "body_hash": "new hash", "status": "working_draft" }
POST /api/v1/schemas/{id}/prepare { "message": "Initial company AI policy" } → { "schema_id": "schema-document-id", "prepared_id": "...", "version": "1.0.0" }
POST /api/v1/schemas/{id}/publish { "prepared_id": "...", "visibility": "private", "changelog": "Initial private release" } → { "schema_id": "schema-document-id", "version": "1.0.0", "visibility": "private" }
Call POST /api/v1/schemas/{id}/drafts to copy the current immutable release into a new working version. The server suggests the next numbered or date-based version. Replace that body if needed, prepare it, then publish it. Published versions are never edited in place.
POST /api/v1/schemas/{schema_id}/drafts → { "schema_id": "schema-document-id", "version": "1.1.0", "body_hash": "...", "status": "working_draft" }
If a new release no longer supports a Flow experience that currently follows latest, ProseID atomically keeps that Flow on the preceding compatible version. An open peer-review request blocks direct API publication until it is finished or withdrawn in the dashboard.
Common stable errors include schema_details_incomplete, invalid_schema_version, draft_changed, schema_not_prepared, schema_not_runnable, visibility_required, publisher_mismatch, email_unverified, review_open, and public_to_private. Publication failures include actionable engine and renderer issues in error.details.issues when available.
Turn a published schema into a reusable Flow
Call POST /api/v1/flows with a secret key carrying the flows scope. Pass the schema_id returned by schema creation. The schema must already be published, and it may be public from any publisher or private and owned by the API key’s organization. flow_type is required and must be form, guided_assessment, determination, or checklist; it must also be compatible with that exact release.
POST /api/v1/flows Idempotency-Key: customer-eu-determination { "schema_id": "schema-document-id", "schema_version": "latest", "flow_type": "determination", "title": "Can I use AI?", "slug": "can-i-use-ai", "visibility": "unlisted", "completion_access": "open", "presentation_theme": "light", "embed_attribution": "full", "signing_mode": "none" } → { "flow_id": "flow-document-id", "id": "flow-document-id", "flow_type": "determination", "status": "active", "pricing": { ...frozen price... }, "duplicate": false }
Optional webhook delivery needs deliver_webhook: true, an HTTPS webhook_url, and a generated whsec_ secret. Email delivery needs deliver_email: true and email_to. Delivery settings are stored separately from the public Flow.
The server derives the publisher route from the organization and freezes the trusted base rate, publisher fee, signing cost, and attribution mode. Request-supplied prices and publisher identities are ignored.
The returned schema_id is the internal ProseID document ID used by Flow creation. It is different from the publisher-qualified logical schema.schema_id stored inside the schema body. Save the returned value. Flow creation returns flow_id and echoes the accepted flow_type.
Run the rules that applied on that date
A schema may contain continuous temporal_map periods. Send effective_at as a real YYYY-MM-DD date to validate or record a historical completion. If omitted, the server uses the current UTC date. Future completion dates are rejected.
ProseID issues today's date in the server manifest and binds it to the completion. A Flow left open across UTC midnight reloads before it can complete against stale rules.
Your trusted backend may supply a present or past date. The selected rule-set name and inclusive period are retained in the record and signed proof.
Time periods must be ordered, continuous, and non-overlapping; only the final period may have an open end. Rules without a rule-set assignment apply during every period. An invalid map or uncovered date fails closed.
Validate without creating a record
Call POST /api/v1/validate/{publisher}/{slug} to run an answer document against the schema currently bound to a published Flow. This route uses the validate scope and is free: it creates no record, proof, delivery, or debit.
POST /api/v1/validate/acme/employee-onboarding { "responses": { "full_name": "Dana Lee", "start_date": "2026-07-01" }, "effective_at": "2026-07-01" }
{
"valid": true,
"status": "READY",
"definitions": { ... },
"effective_at": "2026-07-01",
"temporal_context": { "logic_version": "rules_2026", "valid_range": ["2026-01-01", null] },
"issues": []
}{ "error": {
"code": "validation_failed",
"details": {
"status": "INCOMPLETE",
"issues": [ ... ]
}
} }Record answers you already collected
POST /api/v1/records is a completion endpoint, not the beginning of an interactive Flow. Your backend sends the final answers it already holds; ProseID resolves the published Flow and pinned schema, validates the answers, charges the organization, encrypts the stored record, signs its proof, and returns the completed recordId. To let a respondent complete the experience on ProseID instead, create a Flow link without supplying responses.
POST /api/v1/records { "flow": "acme/employee-onboarding", "responses": { "full_name": "Dana Lee", "start_date": "2026-07-01" }, "effective_at": "2026-07-01", "metadata": { "orderId": "order_8f3a9b2c" } }
{ "recordId": "k9Zx7Qm3nR8pT2vB1cY5wJ", "status": "completed", "interface_language": "en", "costMicrons": 200, "balanceAfterMicrons": 9800, "effective_at": "2026-07-01", "logic_version": "rules_2026", "temporal_range": ["2026-01-01", null], "duplicate": false, "delivered": { "email": false, "webhook": true } }
- flow
- Required unless flow_id is supplied. The publisher-qualified Flow route, for example acme/employee-onboarding. The secret key must belong to the same organization as the Flow.
- flow_id
- Alternative to flow. Use the internal Flow document ID when the Flow has no route slug.
- responses
- Required, non-empty answer map. Each property name is a schema definition (field) ID and its value is the final answer your backend collected. Only fields defined by the resolved schema enter the validated stored record; unknown properties are discarded. Invalid or incomplete answers return 422 with engine issues.
- effective_at
- Optional YYYY-MM-DD date on which the obligation is evaluated. Defaults to the current UTC date. Present and past dates are accepted; future completed records are rejected.
- signature
- Required only when the Flow uses a basic signature: { kind: "basic", typed_name: "Dana Lee", acknowledged: true }. Omit it for unsigned Flows. The Flow controls signing mode; a signed request flag is rejected.
- metadata
- Optional JSON object stored on the record and returned by GET /api/v1/records/:id. Maximum 4KB.
The entire JSON body is limited to 64KB and responses may contain at most 256 properties. balanceAfterMicrons and delivered are returned for a fresh completion; an idempotent replay returns the original ID, status and cost with duplicate: true. Delivery flags report only the immediate email and webhook attempts; eligible failures continue through the retry queue.
Know what failed before you retry
Every non-2xx developer response uses the same error.code, error.message, and optional error.details envelope. Branch on the stable code, not the human message. A rejected completion does not create a completed record or debit the organization’s usage balance.
{
"error": {
"code": "insufficient_balance",
"message": "The organization has insufficient balance.",
"details": {
"availableMicrons": 100,
"availableCashMicrons": 0,
"requiredMicrons": 400,
"requiredCashMicrons": 200
}
}
} availableMicrons is the total usable balance. availableCashMicrons excludes the monthly plan bonus and other promotional balance where a publisher fee, provider-signing cost, or paid white-label charge requires paid funds. Add the missing balance, then safely retry the identical request with its original idempotency key.
invalid_request · invalid_effective_date · signature_not_allowed Correct the request. Retrying the same body will fail again.
unauthorized · publishable_key_not_allowed Replace or correct the credential. Server endpoints require a secret key.
insufficient_balance Add usable balance, then retry with the same idempotency key and body.
forbidden · org_mismatch Use a key with the required scope from the organization that owns the Flow.
flow_not_found · flow_unpublished Stop retrying and confirm that the Flow still exists and is published.
idempotency_conflict · flow_changed · signing_not_available Resolve the conflict. Reload changed Flow details or use a new key for a genuinely different request.
record_unavailable The original record is pending deletion or permanently unavailable. Do not recreate it with the same identity.
payload_too_large Reduce the JSON body below 64KB.
validation_failed · flow_misconfigured · invalid_balance · signature_required · invalid_signature · schema_unavailable Correct the answers, signature, or Flow configuration before retrying.
rate_limited Wait for the Retry-After interval, then retry with backoff.
upstream_unavailable · schema_unavailable A required dependency or release read failed. Retry with bounded exponential backoff.
not_configured · encryption_unavailable · proof_unavailable Wait for service recovery. No completed record is saved or charged when secure storage or proof signing fails.
A fresh success includes balanceAfterMicrons and immediate delivered results. An idempotent replay returns the original record with duplicate: true. Webhook or email failure is different: the record is already valid and billed, the corresponding delivery flag is false, and ProseID retries eligible delivery failures separately.
Retrieve the completed record
Call GET /api/v1/records/{recordId} with the records scope. The server confirms that the key belongs to the record's organization and decrypts the response fields from KMS-backed storage. A missing, foreign, pending-deletion, or deleted record returns the same 404 record_not_found response.
GET /api/v1/records/k9Zx7Qm3nR8pT2vB1cY5wJ { "recordId": "k9Zx7Qm3nR8pT2vB1cY5wJ", "status": "completed", "flow_id": "8f3a9b2c1d4e", "schema_id": "employee-onboarding", "schema_version": "3.0.0", "effective_at": "2026-07-01", "logic_version": "rules_2026", "temporal_range": ["2026-01-01", null], "publisher": "acme", "signed": false, "costMicrons": 200, "completedAt": "2026-07-15T18:42:11.000Z", "responses": { "full_name": "Dana Lee", "start_date": "2026-07-01" }, "metadata": { "orderId": "order_8f3a9b2c" } }
Resolve a schema release
Call GET /api/v1/schemas/{schemaId} with the schemas scope. Add ?version=3.0.0 to pin an immutable release; without it, the route resolves the current version. Public releases are readable by any organization key. Private and draft schemas are readable only by their owning organization.
The response keeps Registry metadata outside the executable schema body: description, legalReferences, and jurisdictions describe the release but never become respondent inputs.
GET /api/v1/schemas/employee-onboarding?version=3.0.0 { "id": "employee-onboarding", "title": "Employee onboarding", "publisher": "acme", "version": "3.0.0", "description": "A 72-hour personal-data-breach notification workflow.", "legalReferences": [{ "instrument": "Regulation (EU) 2016/679 (GDPR)", "provision": "Article 33", "source_url": "https://eur-lex.europa.eu/eli/reg/2016/679/oj" }], "jurisdictions": ["SE", "EEA"], "published": true, "visibility": "public", "deprecated": false, "maintenanceStatus": "maintained", "lineage": null, "schema": { ...canonical schema body... } }
Verify without trusting our API
Call GET /api/v1/records/{recordId}/proof with the proof scope. Current proofs use an ES256 signature whose private key stays inside Google Cloud KMS. The proof's kid selects a public key from our public JWKS. Save that public key set to verify later without calling ProseID. Add ?format=pdf to download the server-rendered proof certificate instead of JSON.
{ "proof_version": 2, "issuer": "https://proseid.com", "canonicalization": "proseid-json-v2", "alg": "ES256", "kid": "public-key-thumbprint", "jwks_uri": "https://proseid.com/.well-known/proseid-jwks.json", "signed_at": "2026-07-15T18:42:11.000Z", "record_id": "…", "flow_id": "…", "schema_id": "employee-onboarding", "schema_version": "3.0.0", "effective_at": "2026-07-01", "logic_version": "rules_2026", "temporal_range": ["2026-01-01", null], "publisher": "acme", "status": "READY", "responses_hash": "…", "output_hash": "…", "completed_at": "2026-07-15T18:42:11.000Z", "signing_provider": "", "signing_reference": "", "signature": "…" }
The kid is part of the signed envelope. When ProseID rotates its signing key, the JWKS retains old public keys so historical proof-v2 records continue to verify.
Webhook delivery
A newly created standard Flow costs $0.20 per completed record (200 microns); validation via /api/v1/validate is free. The accepted base price is frozen on the Flow, so a later platform-price change cannot reprice it. When a Flow completes, ProseID immediately POSTs to the Flow's configured webhook URL with a 10-second timeout. A transient failure enters the durable retry queue. Poll GET /api/v1/records/{id} as the source of truth — webhook delivery is separate from completion and billing.
{ "event": "record.completed", "event_version": 1, "test": false, "record_id": "k9Zx7Qm3nR8pT2vB1cY5wJ", "flow_id": "8f3a9b2c1d4e", "flow_type": "guided_assessment", "interface_language": "en", "schema_id": "employee-onboarding", "schema_version": "3.0.0", "effective_at": "2026-07-01", "logic_version": "rules_2026", "temporal_range": ["2026-01-01", null], "publisher": "acme", "signed": false, "signing_mode": "none", "signing_provider": null, "cost_microns": 200, "source": "hosted", "completed_at": "2026-07-15T18:42:11.000Z", "metadata": null, "responses": { ...Flow answers... }, "signature": null }
Verify the request came from us using the X-Proseid-Signature header — an HMAC-SHA256 of the raw body keyed with your webhook secret. X-Proseid-Event is record.completed. The Flow editor can send the same full payload with test: true, actual fields from the exact schema release, and a test record ID. It shows the final HTTP status and response preview before you save, creates no record, and uses no balance.
- → Unsigned completions use signing_mode "none". Basic typed-name signatures use signing_mode "basic" and remain at the Flow’s frozen standard price (currently $0.20 for new Flows).
- → Each Flow has its own webhook secret. You can reveal or copy it from the Flow editor, and regenerate it later; the old secret stops working when the change is saved.
- → Retries use backoff — roughly 2 min, then 10 min, 1 h, 6 h, and 24 h after the first attempt (6 attempts total). After the last attempt, delivery is marked permanently failed and the organization is notified.
- → Respond 2xx within 10s. Anything else (or a timeout) counts as failed and triggers a retry.
Webhook delivery is configured on the Flow, not per request — a record is billed the moment it completes, whether or not a webhook is set. If you'd rather not use one, just GET /api/v1/records/{id} for the result.
Flow links
Instead of POSTing answers yourself, issue a browser-bound link your end user opens and completes. New hosted Flows require these one-time links by default, so a shareable public preview cannot submit or use your balance; opening completion to anyone with the stable link is an explicit Flow setting. The first browser to open an issued link claims it; only that browser can finish it. The hosted Flow uses the experience selected by its publisher—Standard Form, Guided Assessment, Determination or Compliance Checklist—shows the remaining time, and saves unfinished work in that browser, so a refresh or resent link does not erase it. Nothing is billed until they confirm; the Flow’s frozen per-completion price covers the whole experience, including an entire checklist.
POST /api/v1/flow-links { "flow": "acme/employee-onboarding", "recipient_email": "[email protected]", "send_email": true, "expires_in_days": 14 } → { "token": "…", "url": "https://proseid.com/flow-links/…", "status": "pending", "emailed": true }
Poll GET /api/v1/flow-links/{token} for its state (pending → opened → completed | cancelled | expired); once completed it returns the completedRecordId you read via GET /api/v1/records/{id}.
GET /api/v1/flow-links/{token} → { "token": "…", "status": "completed", "flowId": "8f3a9b2c1d4e", "url": "https://proseid.com/flow-links/…", "recipientEmail": "[email protected]", "completedRecordId": "k9Zx7Qm3nR8pT2vB1cY5wJ", "expiresAt": "2026-07-29T18:42:11.000Z", "createdAt": "2026-07-15T18:42:11.000Z" }
DELETE /api/v1/flow-links/{token} → { "token": "…", "status": "cancelled" }
- → Cancel works even after the link has been opened; an in-progress respondent gets a clean cancelled screen. A completed link cannot be cancelled (409).
- → expires_in_days is optional (default 14, max 365). Past the deadline the link stops working.
- → Use flow_id instead of flow when you have the internal Flow document ID. recipient_name is optional. The complete request body may not exceed 8KB.
- → When send_email is true, recipient_email must be valid. The response reports emailed and, when delivery fails, email_error; the link is still created so you can share its url yourself.
- → All three calls use the records scope and only ever touch your own org’s links.