Documentation

One schema.
Hosted or integrated.

Use the server API to create and publish schemas, create reusable Flows, validate answers, record completions, or issue hosted links.

No-code authorsLearn the visual builder Schema authorsWrite schemas in code
01 — Authentication

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
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 envelope
{
  "error": {
    "code": "validation_failed",
    "message": "the document did not pass validation",
    "details": { ... }
  }
}
02 — Idempotency

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.
03 — Manage schemas

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.

Safe defaults

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.

Management endpoint sequence

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.

1 · Create the working schema
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 }
2 · Replace the working body when configuration changes
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" }
3 · Prepare
POST /api/v1/schemas/{id}/prepare
{ "message": "Initial company AI policy" }

→ { "schema_id": "schema-document-id", "prepared_id": "...", "version": "1.0.0" }
4 · Publish
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" }
Updating a released schema

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.

04 — Create a Flow

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.

Create Flow
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 }
Delivery

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.

Pricing and ownership

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.

05 — Effective dates

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.

Hosted and embedded

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.

Server API

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.

06 — Validate responses

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.

Request
POST /api/v1/validate/acme/employee-onboarding
{
  "responses": { "full_name": "Dana Lee", "start_date": "2026-07-01" },
  "effective_at": "2026-07-01"
}
200 · ready
{
  "valid": true,
  "status": "READY",
  "definitions": { ... },
  "effective_at": "2026-07-01",
  "temporal_context": { "logic_version": "rules_2026", "valid_range": ["2026-01-01", null] },
  "issues": []
}
422 · incomplete or invalid
{ "error": {
  "code": "validation_failed",
  "details": {
    "status": "INCOMPLETE",
    "issues": [ ... ]
  }
} }
07 — Record a completion

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.

Request
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" }
}
Response
{
  "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 }
}
Fields
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.

08 — Responses and errors

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.

HTTP 402
insufficient_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.

400
invalid_request · invalid_effective_date · signature_not_allowed

Correct the request. Retrying the same body will fail again.

401
unauthorized · publishable_key_not_allowed

Replace or correct the credential. Server endpoints require a secret key.

402
insufficient_balance

Add usable balance, then retry with the same idempotency key and body.

403
forbidden · org_mismatch

Use a key with the required scope from the organization that owns the Flow.

404
flow_not_found · flow_unpublished

Stop retrying and confirm that the Flow still exists and is published.

409
idempotency_conflict · flow_changed · signing_not_available

Resolve the conflict. Reload changed Flow details or use a new key for a genuinely different request.

410
record_unavailable

The original record is pending deletion or permanently unavailable. Do not recreate it with the same identity.

413
payload_too_large

Reduce the JSON body below 64KB.

422
validation_failed · flow_misconfigured · invalid_balance · signature_required · invalid_signature · schema_unavailable

Correct the answers, signature, or Flow configuration before retrying.

429
rate_limited

Wait for the Retry-After interval, then retry with backoff.

502
upstream_unavailable · schema_unavailable

A required dependency or release read failed. Retry with bounded exponential backoff.

503
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.

09 — Read a record

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.

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" }
}
10 — Read a schema

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... }
}
11 — Verify a proof

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 envelope
{
  "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": "…"
}
Key rotation

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.

12 — Webhook delivery

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.

Payload — completed record
{
  "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.
Prefer to poll?

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.