Developers · Resources

Resources.

Every resource and endpoint, documented straight from the route code, with a curl example you can copy, paste and run.

Resource

Entities

Legal-entity records (the Alethia entity register). Reads are workspace-scoped passthroughs of the stored documents; mutations run through a dedicated idempotent, revision-fenced command layer rather than the generic CRUD helpers. Restricted entities are hidden from keys without the restricted_records capability.

GET/api/v1/entitiesList entities

Lists the workspace's entities ordered by document id, with cursor paging. Records with restricted: true are filtered out unless the key has the restricted_records capability; stale registryCheck evidence is stripped from each record.

scope: read

Parameters

ParameterInTypeRequiredDescription
limitqueryintegerOptionalPage size. Default 50, maximum 200; non-numeric or non-positive values fall back to the default.
cursorquerystringOptionalOpaque cursor from the previous page's nextCursor. Treat as opaque — for keys without restricted_records it is an encrypted token.

Response

200 — { data: Entity[], nextCursor: string | null }. Each element: The stored entity document: id plus every persisted field (the create/patch fields above, plus server-stamped slug, revision, workspaceId, createdAt, updatedAt and provenance fields). registryCheck (Companies House evidence) is included only while the evidence is current — stale evidence is stripped by stripNonCurrentEntityRegistryCheck.

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid pagination cursor.The cursor is malformed or was tampered with.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/entities" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
POST/api/v1/entitiesCreate an entity

Creates an entity through the idempotent entity-mutation command. Requires an Idempotency-Key header; replays of the same key + body return the original result with Idempotency-Replayed: true. displayName defaults to legalName; jurisdiction defaults to "Other", entityType to "OTHER", status to "active". Unknown keys and server-owned fields are rejected (422). Emits an entity.created webhook via the durable outbox.

scope: write

Request headers

Idempotency-Keyrequired
Client-chosen idempotency token, 8–200 chars, no control characters. Missing → 400.

Request body

Strict object (unknown keys rejected). Validated by PublicApiEntityCreateDataSchema.

FieldTypeRequiredDescription
legalNamestringRequiredLegal name, 1–300 chars. The route pre-checks presence and returns 422 with fields.legalName when missing/blank.
displayNamestringOptionalDisplay name, 1–200 chars. Defaults to legalName when omitted or blank.
jurisdictionstringOptionalJurisdiction. Defaults to "Other".Allowed: "UK", "Luxembourg", "Jersey", "Guernsey", "Isle of Man", "Ireland", "Netherlands", "Belgium", "France", "Germany", "Austria", "Switzerland", "Liechtenstein", "Spain", "Portugal", "Italy", "Malta", "Cyprus", "Greece", "Poland", "Czechia", "Slovakia", "Hungary", "Romania", "Bulgaria", "Croatia", "Slovenia", "Estonia", "Latvia", "Lithuania", "Denmark", "Sweden", "Finland", "Norway", "Iceland", "Cayman Islands", "British Virgin Islands", "Bermuda", "Gibraltar", "Mauritius", "Other"
jurisdictionCodestringOptionalStable ISO 3166-1 alpha-2 country scope; required for structured ISO 20275 legal forms.
entityTypestringOptionalEntity type. Defaults to "OTHER".Allowed: "UK_LTD", "UK_PLC", "UK_LLP", "UK_LP", "LU_SCSP", "LU_SCS", "LU_SARL", "LU_SA", "LU_RAIF", "LU_SECURITISATION", "JE_COMPANY", "JE_LP", "JE_UNIT_TRUST", "KY_EXEMPTED_CO", "KY_ORDINARY_CO", "KY_SPC", "KY_ELP", "KY_LP", "KY_LLC", "KY_FOUNDATION_CO", "KY_TRUST", "KY_UNIT_TRUST", "VG_BC_SHARES", "VG_BC_GUARANTEE", "VG_BC_UNLIMITED", "VG_BC_RESTRICTED_PURPOSE", "VG_SPC", "VG_LP", "VG_VISTA_TRUST", "OTHER"
statusstringOptionalLifecycle status. Defaults to "active".Allowed: "active", "dormant", "in_liquidation", "sold", "dissolved", "proposed", "historic", "advisory", "archived"
legalFormstringOptionalLegal form label (max 160 chars).
legalFormElfCodestringOptionalCountry-scoped four-character ISO 20275 ELF code.
legalFormJurisdictionCodestringOptionalGLEIF accepted legal jurisdiction (country or ISO 3166-2 subdivision) for the ELF identity.
legalFormVariantCodestringOptionalJurisdiction-qualified convenience variant key; never implies cross-country equivalence.
legalFormNameOriginalstringOptionalExact native/source or Custom/Other text (max 3000 chars).
legalFormFamilystringOptionalBroad product behaviour family; unclassified is retained when no reviewed mapping exists.Allowed: "company", "partnership", "cooperative", "association", "foundation", "trust", "fund", "public-law-body", "sole-trader", "branch", "supranational-eu", "other", "unclassified"
legalFormSourcestringOptionalStructured legal-form provenance; product overlays are never represented as raw ISO labels.Allowed: "ISO_20275", "product_overlay", "custom"
legalFormSourceVersionstringOptionalPinned provenance version: ISO 20275 1.6 or the versioned product-overlay mapping.
legalSubdivisionCodestringOptionalISO 3166-2 legal system whose law governs the entity (for example GB-SCT, BA-SRP). Must belong to the entity's jurisdictionCode. Chosen by the user, never inferred from an address or the registered office.
legalFormDesignationsarray<string>OptionalStable lowercase designation codes layered on the underlying form (for example pflp). Up to 10, unique.
governanceModelstringOptionalHow management is organised under the constitution. Chosen by the user, never inferred from the country; offices that depend on it are withheld until it is known.Allowed: "sole-administrator", "joint-and-several-administrators", "joint-administrators", "board-of-administrators", "one-tier-board", "two-tier-board", "board-and-control-body", "member-managed", "general-partner-managed", "managing-company", "statute-defined"
governanceModelNotestringOptionalWhere the governance model was established: constitution article, registry extract or resolution (max 1000 chars). Stored exactly as written.
registrationNumberstringOptionalRegistry number (max 100 chars).
registryTypestringOptionalRegister family/code, distinct from the registration number (max 50 chars).
registryAuthoritystringOptionalRegister court or authority (max 300 chars).
registeredSeatstringOptionalLegal/statutory seat, distinct from postal address (max 300 chars).
headOfficeSiretstringOptionalFrench head-office SIRET; distinct from SIREN and tax reference (max 50 chars).
rneRegistrationstringOptionalFrench RNE registration/evidence reference (max 300 chars).
rcsRegistrationstringOptionalFrench conditional RCS registration/city text (max 300 chars).
apeNafCodestringOptionalFrench APE/NAF activity code (max 50 chars).
eoristringOptionalOptional EORI identifier (max 50 chars).
economicIdentificationNumberstringOptionalOptional German W-IdNr. (max 100 chars).
incorporationDatestringOptionalISO date (YYYY-MM-DD).
registeredOfficeobjectOptionalAddress object: line1 (required), line2, city, county, region, postalCode, optional ISO countryCode, and exact country text (required). Full-object replace.
taxReferencestringOptionalTax reference (max 100 chars).
vatNumberstringOptionalVAT number (max 100 chars).
leistringOptionalLegal Entity Identifier — exactly 20 letters/digits.
yearEndstringOptionalAccounting year end (max 100 chars).
regionstringOptionalRegion label (max 120 chars).
businessPurposestringOptionalBusiness purpose (max 5,000 chars).
agmDatestringOptionalAGM date text (max 200 chars).
fiscalYearEndstringOptionalFiscal year end (max 32 chars).
internationalLocalCounselstringOptionalLocal counsel (max 300 chars).
significantSubsidiarybooleanOptionalSignificant-subsidiary flag.
taxTypestringOptionalTax type label (max 120 chars).
dbaNamesarray<string>OptionalTrading names (max 50, unique case-insensitively).
auditorsstringOptionalAuditors (max 500 chars).
administratorsstringOptionalAdministrators (max 500 chars).
lawyersstringOptionalLawyers (max 500 chars).
registeredAgentstringOptionalRegistered agent (max 500 chars).
serviceProviderIdsarray<string>OptionalPerson/entity ids of service providers (max 100, unique).
colourstringOptionalSix-digit hex colour, e.g. #4A6741.
fundNavobjectOptionalLatest NAV snapshot: { value: number ≥ 0, currency: ISO-4217 code, asOf: ISO date }. Full-object replace.
fatcaCrsobjectOptionalFATCA/CRS classification: fatcaStatus, giin, crsStatus, crsClassification, selfCertifiedAt (ISO date), notes — at least one field. Full-object replace.
tagsarray<string>OptionalTag ids (max 100, unique).
previousNamesarray<object>OptionalPrevious names: { name (required), from?: ISO date, to?: ISO date } (max 100).
customFieldsobjectOptionalWorkspace-defined custom fields: string values (max 2,000 chars), at most 100 keys.
restrictedbooleanOptionalRecord-level ACL flag. Changing it requires the restricted_records capability.
viewerIdsarray<string>OptionalExplicit viewer ids for restricted records (max 500). Changing it requires the restricted_records capability.
notesstringOptionalFree notes (max 10,000 chars).

Response

201 — The created entity record when the key also has read scope; otherwise only { id, revision? }. The stored entity document: id plus every persisted field (the create/patch fields above, plus server-stamped slug, revision, workspaceId, createdAt, updatedAt and provenance fields). registryCheck (Companies House evidence) is included only while the evidence is current — stale evidence is stripped by stripNonCurrentEntityRegistryCheck.

Headers: Idempotency-Replayed, ETag

Errors (13)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestIdempotency-Key header is required.The Idempotency-Key header is absent.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
409conflictEntity conflict | Entity limit reached. | Idempotency key conflicts with an earlier request.A conflicting entity exists, the free-tier entity cap is reached, or the Idempotency-Key was already used with a different request body.
503service_unavailableEntity mutations are temporarily unavailable.The workspace's entity-mutation mode is transitioning (Retry-After: 5 is set).
503service_unavailableEntity state requires reconciliation.Persisted entity/receipt state failed integrity checks (logged for operators).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl -X POST "https://app.alethiahq.com/api/v1/entities" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f2d6b7a-9d61-4e3a-8c2a-5e7b1f0a6c9d" \
  -d '{
  "legalName": "example"
}'
GET/api/v1/entities/{entityId}Get an entity

Reads one entity. Restricted entities are indistinguishable from absent ones for keys without restricted_records. The response carries an ETag holding the entity revision, usable in a later PATCH's If-Match.

scope: read

Parameters

ParameterInTypeRequiredDescription
entityIdpathstringRequiredEntity document id.

Response

200 — The stored entity document: id plus every persisted field (the create/patch fields above, plus server-stamped slug, revision, workspaceId, createdAt, updatedAt and provenance fields). registryCheck (Companies House evidence) is included only while the evidence is current — stale evidence is stripped by stripNonCurrentEntityRegistryCheck.

Headers: ETag

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/entities/entity_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
PATCH/api/v1/entities/{entityId}Update an entity

Optimistically-concurrent profile patch. Requires Idempotency-Key and If-Match (the entity revision, bare or quoted, e.g. "3"). On revision mismatch the route returns 412 with currentRevision in the body and an ETag of the current revision. null clears nullable optional fields. Emits entity.updated via the durable outbox.

scope: write

Parameters

ParameterInTypeRequiredDescription
entityIdpathstringRequiredEntity document id.

Request headers

Idempotency-Keyrequired
Client-chosen idempotency token, 8–200 chars. Missing → 400.
If-Matchrequired
Expected entity revision (numeric, optionally quoted). Missing → 428; malformed → 400.

Request body

Strict object (unknown keys rejected); at least one field. Validated by EntityProfilePatchSchema — identity fields are optional but not nullable; profile fields accept null to clear.

FieldTypeRequiredDescription
legalNamestringOptionalLegal name, 1–300 chars.
displayNamestringOptionalDisplay name, 1–200 chars.
jurisdictionstringOptionalJurisdiction.Allowed: "UK", "Luxembourg", "Jersey", "Guernsey", "Isle of Man", "Ireland", "Netherlands", "Belgium", "France", "Germany", "Austria", "Switzerland", "Liechtenstein", "Spain", "Portugal", "Italy", "Malta", "Cyprus", "Greece", "Poland", "Czechia", "Slovakia", "Hungary", "Romania", "Bulgaria", "Croatia", "Slovenia", "Estonia", "Latvia", "Lithuania", "Denmark", "Sweden", "Finland", "Norway", "Iceland", "Cayman Islands", "British Virgin Islands", "Bermuda", "Gibraltar", "Mauritius", "Other"
jurisdictionCodestringOptionalStable ISO 3166-1 alpha-2 country scope.
entityTypestringOptionalEntity type.Allowed: "UK_LTD", "UK_PLC", "UK_LLP", "UK_LP", "LU_SCSP", "LU_SCS", "LU_SARL", "LU_SA", "LU_RAIF", "LU_SECURITISATION", "JE_COMPANY", "JE_LP", "JE_UNIT_TRUST", "KY_EXEMPTED_CO", "KY_ORDINARY_CO", "KY_SPC", "KY_ELP", "KY_LP", "KY_LLC", "KY_FOUNDATION_CO", "KY_TRUST", "KY_UNIT_TRUST", "VG_BC_SHARES", "VG_BC_GUARANTEE", "VG_BC_UNLIMITED", "VG_BC_RESTRICTED_PURPOSE", "VG_SPC", "VG_LP", "VG_VISTA_TRUST", "OTHER"
statusstringOptionalLifecycle status.Allowed: "active", "dormant", "in_liquidation", "sold", "dissolved", "proposed", "historic", "advisory", "archived"
legalFormstring| nullOptionalLegal form label (max 160 chars).
legalFormElfCodestring| nullOptionalCountry-scoped four-character ISO 20275 ELF code.
legalFormJurisdictionCodestring| nullOptionalGLEIF accepted legal jurisdiction (country or ISO 3166-2 subdivision) for the ELF identity.
legalFormVariantCodestring| nullOptionalJurisdiction-qualified convenience variant key; never implies cross-country equivalence.
legalFormNameOriginalstring| nullOptionalExact native/source or Custom/Other text (max 3000 chars).
legalFormFamilystring| nullOptionalBroad product behaviour family; unclassified is retained when no reviewed mapping exists.Allowed: "company", "partnership", "cooperative", "association", "foundation", "trust", "fund", "public-law-body", "sole-trader", "branch", "supranational-eu", "other", "unclassified"
legalFormSourcestring| nullOptionalStructured legal-form provenance; product overlays are never represented as raw ISO labels.Allowed: "ISO_20275", "product_overlay", "custom"
legalFormSourceVersionstring| nullOptionalPinned provenance version: ISO 20275 1.6 or the versioned product-overlay mapping.
legalSubdivisionCodestring| nullOptionalISO 3166-2 legal system whose law governs the entity (for example GB-SCT, BA-SRP). Must belong to the entity's jurisdictionCode. Chosen by the user, never inferred from an address or the registered office.
legalFormDesignationsarray<string>| nullOptionalStable lowercase designation codes layered on the underlying form (for example pflp). Up to 10, unique.
governanceModelstring| nullOptionalHow management is organised under the constitution. Chosen by the user, never inferred from the country; offices that depend on it are withheld until it is known.Allowed: "sole-administrator", "joint-and-several-administrators", "joint-administrators", "board-of-administrators", "one-tier-board", "two-tier-board", "board-and-control-body", "member-managed", "general-partner-managed", "managing-company", "statute-defined"
governanceModelNotestring| nullOptionalWhere the governance model was established: constitution article, registry extract or resolution (max 1000 chars). Stored exactly as written.
registrationNumberstring| nullOptionalRegistry number (max 100 chars).
registryTypestring| nullOptionalRegister family/code, distinct from the registration number (max 50 chars).
registryAuthoritystring| nullOptionalRegister court or authority (max 300 chars).
registeredSeatstring| nullOptionalLegal/statutory seat, distinct from postal address (max 300 chars).
headOfficeSiretstring| nullOptionalFrench head-office SIRET; distinct from SIREN and tax reference (max 50 chars).
rneRegistrationstring| nullOptionalFrench RNE registration/evidence reference (max 300 chars).
rcsRegistrationstring| nullOptionalFrench conditional RCS registration/city text (max 300 chars).
apeNafCodestring| nullOptionalFrench APE/NAF activity code (max 50 chars).
eoristring| nullOptionalOptional EORI identifier (max 50 chars).
economicIdentificationNumberstring| nullOptionalOptional German W-IdNr. (max 100 chars).
incorporationDatestring| nullOptionalISO date (YYYY-MM-DD).
registeredOfficeobject| nullOptionalAddress object: line1 (required), line2, city, county, region, postalCode, optional ISO countryCode, and exact country text (required). Full-object replace.
taxReferencestring| nullOptionalTax reference (max 100 chars).
taxResidenceobject| nullOptionalWhere the entity is resident for tax, which is not inferred from jurisdiction: { jurisdictionCodes: one or more unique ISO 3166-1 alpha-2 codes (max 10), asOf?: ISO date, source?: { kind: "certificate", documentId } | { kind: "statedByUs" } }. The server stamps recordedBy and recordedAt. Full-object replace.
industryobject| nullOptionalWhat the entity does, as a NACE Rev. 2.1 section a person recorded: { scheme: "nace-2.1", section: one of A to V, since?: ISO date }. Full-object replace; null clears it. Never filled from a registry code by the API.
vatNumberstring| nullOptionalVAT number (max 100 chars).
leistring| nullOptionalLegal Entity Identifier — exactly 20 letters/digits.
yearEndstring| nullOptionalAccounting year end (max 100 chars).
regionstring| nullOptionalRegion label (max 120 chars).
businessPurposestring| nullOptionalBusiness purpose (max 5,000 chars).
agmDatestring| nullOptionalAGM date text (max 200 chars).
fiscalYearEndstring| nullOptionalFiscal year end (max 32 chars).
internationalLocalCounselstring| nullOptionalLocal counsel (max 300 chars).
significantSubsidiaryboolean| nullOptionalSignificant-subsidiary flag.
taxTypestring| nullOptionalTax type label (max 120 chars).
dbaNamesarray<string>| nullOptionalTrading names (max 50, unique case-insensitively).
auditorsstring| nullOptionalAuditors (max 500 chars).
administratorsstring| nullOptionalAdministrators (max 500 chars).
lawyersstring| nullOptionalLawyers (max 500 chars).
registeredAgentstring| nullOptionalRegistered agent (max 500 chars).
serviceProviderIdsarray<string>| nullOptionalPerson/entity ids of service providers (max 100, unique).
colourstring| nullOptionalSix-digit hex colour, e.g. #4A6741.
fundNavobject| nullOptionalLatest NAV snapshot: { value: number ≥ 0, currency: ISO-4217 code, asOf: ISO date }. Full-object replace.
fatcaCrsobject| nullOptionalFATCA/CRS classification: fatcaStatus, giin, crsStatus, crsClassification, selfCertifiedAt (ISO date), notes — at least one field. Full-object replace.
tagsarray<string>OptionalTag ids (max 100, unique).
previousNamesarray<object>| nullOptionalPrevious names: { name (required), from?: ISO date, to?: ISO date } (max 100).
customFieldsobject| nullOptionalWorkspace-defined custom fields: string values (max 2,000 chars), at most 100 keys.
restrictedbooleanOptionalRecord-level ACL flag. Changing it requires the restricted_records capability.
viewerIdsarray<string>OptionalExplicit viewer ids for restricted records (max 500). Changing it requires the restricted_records capability.
notesstring| nullOptionalFree notes (max 10,000 chars).

Response

200 — The updated entity record when the key also has read scope; otherwise only { id, revision? }. The stored entity document: id plus every persisted field (the create/patch fields above, plus server-stamped slug, revision, workspaceId, createdAt, updatedAt and provenance fields). registryCheck (Companies House evidence) is included only while the evidence is current — stale evidence is stripped by stripNonCurrentEntityRegistryCheck.

Headers: Idempotency-Replayed, ETag

Errors (16)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestIdempotency-Key header is required. | If-Match must contain one numeric entity revision.The Idempotency-Key header is absent, or If-Match is present but not a single non-negative integer.
428precondition_requiredIf-Match header is required.The If-Match header is absent.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
412precondition_failedEntity revision does not match.If-Match does not equal the stored revision. Body carries currentRevision; ETag header carries the current revision.
409conflictEntity conflict | Entity limit reached. | Idempotency key conflicts with an earlier request.A conflicting entity exists, the free-tier entity cap is reached, or the Idempotency-Key was already used with a different request body.
503service_unavailableEntity mutations are temporarily unavailable.The workspace's entity-mutation mode is transitioning (Retry-After: 5 is set).
503service_unavailableEntity state requires reconciliation.Persisted entity/receipt state failed integrity checks (logged for operators).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl -X PATCH "https://app.alethiahq.com/api/v1/entities/entity_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f2d6b7a-9d61-4e3a-8c2a-5e7b1f0a6c9d" \
  -H "If-Match: 3" \
  -d '{
  "legalName": "example"
}'
DELETE/api/v1/entities/{entityId}Delete an entity (approved deletions only)

Executes a previously APPROVED four-eyes deletion request. A valid entityDeletionRequests record (requested and approved in-app by two different members) must exist for the entity; without one — or when the entity is restricted and the key lacks restricted_records — the route returns 404. On success the entity and its references are removed and entity.deleted is emitted via the durable outbox. This endpoint does not support If-Match (or Idempotency-Key): the approved request, not a revision the caller read, authorises the deletion.

scope: write

Parameters

ParameterInTypeRequiredDescription
entityIdpathstringRequiredEntity document id.

Response

200 — { deleted: true, id: string }

Errors (10)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundThe entity does not exist, has no approved deletion request, or is a restricted record the key cannot access.
409record_retainedThis entity cannot be deleted while a legal hold is in place.A legal hold exists in the workspace. Nothing is deleted; a deletion already under way stops with the entity kept.
409record_retainedThis entity cannot be deleted because other records depend on it. Archive it instead.Other records depend on the entity. Nothing is deleted; a deletion already under way stops with the entity kept. Archive the entity instead.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl -X DELETE "https://app.alethiahq.com/api/v1/entities/entity_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"

Resource

People

Directory of individuals and corporate contacts. Personal-details fields (email, phone, nationality, countryOfResidence, residentialAddress) are ALWAYS masked in responses and REJECTED in request bodies — an API key never carries access to personal details.

GET/api/v1/peopleList people

Lists people with cursor paging. Every record is passed through maskPerson, blanking the five personal-details fields.

scope: read

Parameters

ParameterInTypeRequiredDescription
limitqueryintegerOptionalPage size. Default 50, maximum 200; non-numeric or non-positive values fall back to the default.
cursorquerystringOptionalOpaque cursor from the previous page's nextCursor. Treat as opaque — for keys without restricted_records it is an encrypted token.

Response

200 — { data: Person[], nextCursor: string | null }. Each element: id, fullName, type, firstName, lastName, companyId, title, isServiceProvider, status, tags, piiSeparated, workspaceId, createdAt, updatedAt, provenance fields — personal-details fields removed.

Errors (7)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/people" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
POST/api/v1/peopleCreate a person

Creates a person. Any supplied personal-details field is rejected with 422 ("Personal details are not accepted by this API key"). type defaults to "individual"; the server stamps piiSeparated: true. Fires the person.created webhook.

scope: write

Request headers

Idempotency-Key
Optional client-chosen token, 8–200 chars, no control characters. The same key with the same request replays the original 2xx response (Idempotency-Replayed: true) without repeating the write; the same key with a different request → 409. Keys are scoped to the API key, remembered for 24 hours, and only a successful response is stored (a failed request frees the key). Malformed → 400.

Request body

Strict object validated by PublicApiPersonCreateSchema.

FieldTypeRequiredDescription
fullNamestringRequiredFull name, 1–300 chars.
typestringOptionalDefaults to "individual".Allowed: "individual", "corporate"
firstNamestringOptionalMax 150 chars.
lastNamestringOptionalMax 150 chars.
companyIdstringOptionalLinked company (entity/person) id.
titlestringOptionalJob title, max 150 chars.
isServiceProviderbooleanOptionalMarks the person as a service provider.
statusstringOptionalDirectory status.Allowed: "active", "archived"
tagsarray<string>OptionalTag ids (max 100, unique).

Response

201 — The created person record (id, submitted fields, piiSeparated: true, workspaceId, createdAt, updatedAt, createdVia: "api").

Headers: Idempotency-Replayed, ETag

Errors (12)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIdempotency-Key must be 8-200 characters with no control characters or surrounding whitespace.An Idempotency-Key header is present but malformed.
409conflictIdempotency key conflicts with an earlier request. | A request with this Idempotency-Key is still being processed.The key was already used with a different request, or the first request with it is still running (Retry-After is set).

Example request

curl -X POST "https://app.alethiahq.com/api/v1/people" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f2d6b7a-9d61-4e3a-8c2a-5e7b1f0a6c9d" \
  -d '{
  "fullName": "example"
}'
GET/api/v1/people/{personId}Get a person

Reads one person, personal details masked.

scope: read

Parameters

ParameterInTypeRequiredDescription
personIdpathstringRequiredPerson document id.

Response

200 — The person record with personal-details fields removed.

Headers: ETag

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/people/person_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
PATCH/api/v1/people/{personId}Update a person

Merge-patches a person. personal-details fields are rejected with 422. Nullable fields (firstName, lastName, companyId, title) accept null to clear. Fires person.updated.

scope: write

Parameters

ParameterInTypeRequiredDescription
personIdpathstringRequiredPerson document id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Request body

Strict object, at least one field (PublicApiPersonPatchSchema).

FieldTypeRequiredDescription
fullNamestringOptionalFull name, 1–300 chars.
typestringOptionalPerson type.Allowed: "individual", "corporate"
firstNamestring| nullOptionalMax 150 chars; null clears.
lastNamestring| nullOptionalMax 150 chars; null clears.
companyIdstring| nullOptionalLinked company id; null clears.
titlestring| nullOptionalMax 150 chars; null clears.
isServiceProviderbooleanOptionalService-provider flag.
statusstringOptionalDirectory status.Allowed: "active", "archived"
tagsarray<string>OptionalTag ids (max 100, unique).

Response

200 — The merged person record, personal details masked.

Headers: ETag

Errors (13)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X PATCH "https://app.alethiahq.com/api/v1/people/person_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "If-Match: 3" \
  -d '{
  "fullName": "example"
}'
DELETE/api/v1/people/{personId}Delete a person

Permanently deletes a person and their private contact record, and fires person.deleted. Hard delete is only for people who are not referenced by any other record, such as a person created by mistake. If the person is referenced, or if the workspace holds records that cannot be checked person by person (for example KYC or compliance records that may be subject to retention requirements), the request is refused with 409 person_referenced and nothing is changed; archive the person instead (PATCH status: "archived"). The response does not say which records refer to the person. GDPR erasure requests and retention-based deletion are not done through this endpoint: they are handled by a separate, controlled SEC-4 workflow.

scope: write

Parameters

ParameterInTypeRequiredDescription
personIdpathstringRequiredPerson document id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Response

200 — { deleted: true, id: string }

Errors (11)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
409person_referencedThis person cannot be deleted because other records refer to them. Archive the person instead.Other records refer to the person, or the workspace holds records for which it cannot be confirmed that they do not (for example KYC records stored in nested lists, or more structure-chart links, provider records or deals than one request can check). The body is the same whatever the reason and adds archive: { method, path, body }, the PATCH that archives the person. No data is changed; archiving is the default for a referenced person.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X DELETE "https://app.alethiahq.com/api/v1/people/person_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "If-Match: 3"
POST/api/v1/people/{personId}/erasureErase a person's personal data (not available)

Always refused with 501 erasure_not_available; nothing is read or changed. GDPR erasure of a data subject's personal data is a controlled, retention-aware SEC-4 process that weighs legal holds and retention periods against the person's records, and it is not available through the API. Archiving (PATCH status: "archived") hides a person and keeps every record; DELETE removes only a person nothing refers to. Neither is erasure.

scope: write

Parameters

ParameterInTypeRequiredDescription
personIdpathstringRequiredPerson document id.

Request headers

Idempotency-Key
Optional client-chosen idempotency token, 8–200 chars; a replay returns the same refusal.

Response

501 — { error: string, code: "erasure_not_available" }

Headers: Idempotency-Replayed

Errors (6)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
501erasure_not_availableErasure of a person's personal data is not available through the API. It is a controlled, retention-aware process (SEC-4) that weighs legal holds and retention periods; ask the workspace administrator to raise an erasure request. To stop using a person's record now, archive it (PATCH status: archived).Always. Nothing is changed.

Example request

curl -X POST "https://app.alethiahq.com/api/v1/people/person_123/erasure" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Idempotency-Key: 9f2d6b7a-9d61-4e3a-8c2a-5e7b1f0a6c9d"

Resource

Deals

Acquisition-pipeline deals (the Pipeline module). Workflow fields — stage, locked, icSubmission, parentHoldcoId, activity, foundationDesignId — are service-layer-owned: rejected as unknown keys on CREATE and silently stripped on PATCH, so stage transitions and IC state can only change through the app. acquiringEntityId (the buying company) may be set to an EXISTING company of the workspace on CREATE, or on PATCH while the deal has none; the draft company the app creates at Heads of Terms is never settable. A key without the restricted_records capability gets no record here that belongs to or names a restricted entity, or an entity that does not exist (for a shareholding: its issuer and an entity holder; for a deal: its buying company and the entities its parties act for). Such a record is absent from lists and answers 404 on GET (and on PATCH and DELETE where the resource offers them), exactly as a missing record does; where the resource offers create or update, naming such an entity answers 422, exactly as naming a missing one does.

GET/api/v1/dealsList deals

Lists deals with cursor paging.

scope: read

Parameters

ParameterInTypeRequiredDescription
limitqueryintegerOptionalPage size. Default 50, maximum 200; non-numeric or non-positive values fall back to the default.
cursorquerystringOptionalOpaque cursor from the previous page's nextCursor. Treat as opaque — for keys without restricted_records it is an encrypted token.

Response

200 — { data: Deal[], nextCursor: string | null }. Each element: The stored deal document: id plus the fields above, stage, activity (audit trail; the create route stamps an initial 'Deal added to the pipeline' entry), defaulted parties/tags arrays and GBP currency, any workflow fields set by the service layer (locked, icSubmission, parentHoldcoId, reference, foundationDesignId), acquiringEntityId when a buying company was named or the app created a draft one, workspaceId and audit stamps (createdAt, updatedAt, createdVia: "api").

Errors (7)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/deals" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
POST/api/v1/dealsCreate a deal

Creates a deal. nation must be England, Wales or Scotland. stage (optional, default "identified") is validated against the workspace's RESOLVED pipeline — canonical plus custom stage ids. Domain defaults (parties/activity/tags arrays, GBP) and an initial activity entry are applied server-side. Fires deal.created.

scope: write

Request headers

Idempotency-Key
Optional client-chosen token, 8–200 chars, no control characters. The same key with the same request replays the original 2xx response (Idempotency-Replayed: true) without repeating the write; the same key with a different request → 409. Keys are scoped to the API key, remembered for 24 hours, and only a successful response is stored (a failed request frees the key). Malformed → 400.

Request body

Strict object validated by PublicApiDealCreateSchema. Server-owned workflow fields (reference, locked, icSubmission, parentHoldcoId, activity, foundationDesignId) are rejected as unknown keys.

FieldTypeRequiredDescription
namestringRequiredDeal name, 1–300 chars.
nationstringRequiredUK nation. Any other value → 422.Allowed: "England", "Wales", "Scotland"
stagestringOptionalPipeline stage id, validated against the workspace's resolved stages (canonical + custom). Defaults to "identified".
acquiringEntityIdstringOptionalId of an EXISTING company in this workspace that is buying the site. It must exist in the key's workspace, be readable by the key (restricted companies need the restricted_records capability) and be one of your own companies that has not ended or been archived; otherwise 422 on this field. The link is logged in the deal's activity, and the company itself is never changed by it.
slugstringOptionalURL slug (max 200 chars).
siteAddressobjectOptionalAddress: line1 (required), line2, city, county, region, postalCode, country (required).
assetClassstringOptionalAsset-class id from the workspace's ASSET_CLASS_LABELS set.
investmentTypestringOptionalInvestment-type id from INVESTMENT_TYPE_LABELS.
proposedEntityNamestringOptionalProposed acquiring-entity name (max 300 chars).
proposedEntityJurisdictionstringOptionalProposed entity jurisdiction (max 100 chars).
pricenumberOptionalPrice ≥ 0.
currencystringOptionalCurrency code (max 10 chars). Defaults to GBP on create.
siteSizeobjectOptional{ value: number ≥ 0, unit: sqm | sqft | acre }.
depositobjectOptional{ kind: percent | amount, value: number ≥ 0 }.
purchaseConditionsarray<object>OptionalArray (max 300) of { id, label, met: boolean }.
targetsarray<object>OptionalArray (max 300) of milestones { id, label, kind: stage | custom, stage?, targetDate?, actualDate? (ISO dates) }.
partiesarray<object>OptionalArray (max 300) of { role (deal-party role id), name, companyId?, companyName?, personId?, entityId?, contact? }.
keyDatesobjectOptional{ hotSigned?, exclusivityEnds?, targetExchange?, exchanged?, targetCompletion?, completed?, planningDecisionExpectedDate? } — all ISO dates.
appraisalobjectOptional{ gdv?: number ≥ 0, totalCost?: number ≥ 0 }.
appraisalScenariosarray<object>OptionalArray (max 50) of { name, gdv?, totalCost?, holdingYears? }.
sourcestringOptionalDeal source (max 300 chars).
regionMarketstringOptionalRegion/market label (max 200 chars).
askingPricenumberOptionalAsking price ≥ 0.
agreedPricenumberOptionalAgreed price ≥ 0.
headlineMetricTypestringOptionalHeadline metric type (max 100 chars).
headlineMetricValuestringOptionalHeadline metric value (max 100 chars).
unitCountnumberOptionalUnit count ≥ 0.
businessPlanSummarystringOptionalBusiness-plan summary (max 5,000 chars).
keyRiskstringOptionalKey risk (max 2,000 chars).
nextActionTextstringOptionalNext action (max 500 chars).
nextActionDueDatestringOptionalISO date.
hotsStatusstringOptionalHoTs status id from HOTS_STATUS.
planningStatusstringOptionalPlanning status id from PLANNING_STATUS.
currentPlanningStatusstringOptionalCurrent planning status id from CURRENT_PLANNING_STATUS_LABELS.
creditTermsobjectOptional{ borrowerSponsor?, facilityType?, loanAmount?, ltv?, marginCoupon?, termMaturity?, seniority?, securityCollateral?, covenantSummary? }.
abortReasonstringOptionalAbort reason (max 1,000 chars).
ddStatusstringOptionalDue-diligence status id from DD_STATUS.
legalStatusstringOptionalLegal status id from LEGAL_STATUS.
prioritystringOptionalPriority id from DEAL_PRIORITY.
customFieldsobjectOptionalWorkspace-defined custom fields: string | number | boolean | null values, at most 100 keys.
dueDiligencearray<object>OptionalArray (max 300) of { id, label, done: boolean, category? }.
tasksarray<object>OptionalArray (max 300) of { id, title, dueDate? (ISO date), done: boolean }.
projectIdstringOptionalLinked project id.
tagsarray<string>OptionalTag ids (max 100, unique).
colourstringOptionalSix-digit hex colour.
notesstringOptionalFree notes (max 10,000 chars).

Response

201 — The stored deal document: id plus the fields above, stage, activity (audit trail; the create route stamps an initial 'Deal added to the pipeline' entry), defaulted parties/tags arrays and GBP currency, any workflow fields set by the service layer (locked, icSubmission, parentHoldcoId, reference, foundationDesignId), acquiringEntityId when a buying company was named or the app created a draft one, workspaceId and audit stamps (createdAt, updatedAt, createdVia: "api").

Headers: Idempotency-Replayed, ETag

Errors (12)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIdempotency-Key must be 8-200 characters with no control characters or surrounding whitespace.An Idempotency-Key header is present but malformed.
409conflictIdempotency key conflicts with an earlier request. | A request with this Idempotency-Key is still being processed.The key was already used with a different request, or the first request with it is still running (Retry-After is set).

Example request

curl -X POST "https://app.alethiahq.com/api/v1/deals" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f2d6b7a-9d61-4e3a-8c2a-5e7b1f0a6c9d" \
  -d '{
  "name": "example",
  "nation": "England"
}'
GET/api/v1/deals/{dealId}Get a deal

Reads one deal (raw stored document).

scope: read

Parameters

ParameterInTypeRequiredDescription
dealIdpathstringRequiredDeal document id.

Response

200 — The stored deal document: id plus the fields above, stage, activity (audit trail; the create route stamps an initial 'Deal added to the pipeline' entry), defaulted parties/tags arrays and GBP currency, any workflow fields set by the service layer (locked, icSubmission, parentHoldcoId, reference, foundationDesignId), acquiringEntityId when a buying company was named or the app created a draft one, workspaceId and audit stamps (createdAt, updatedAt, createdVia: "api").

Headers: ETag

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/deals/deal_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
PATCH/api/v1/deals/{dealId}Update a deal

Merge-patches a deal. Accepts everything CREATE does (nullable to clear) plus reference; the workflow fields (stage, locked, icSubmission, parentHoldcoId, activity, foundationDesignId) are ACCEPTED in the body — so read-modify-write round-trips don't 422 — but silently stripped from the write. acquiringEntityId is applied only when the deal has no buying company yet (same checks as create, logged in activity; a locked deal is refused); on a deal that already has one it is stripped like an echo. Fires deal.updated.

scope: write

Parameters

ParameterInTypeRequiredDescription
dealIdpathstringRequiredDeal document id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Request body

Strict object, at least one field (PublicApiDealPatchSchema).

FieldTypeRequiredDescription
namestringOptionalDeal name, 1–300 chars.
nationstringOptionalBounded string on PATCH (the England/Wales/Scotland value check runs on create only).
referencestring| nullOptionalDeal reference (settable post-create; protected on create).
slugstring| nullOptionalURL slug (max 200 chars).
siteAddressobject| nullOptionalAddress: line1 (required), line2, city, county, region, postalCode, country (required).
assetClassstring| nullOptionalAsset-class id from the workspace's ASSET_CLASS_LABELS set.
investmentTypestring| nullOptionalInvestment-type id from INVESTMENT_TYPE_LABELS.
proposedEntityNamestring| nullOptionalProposed acquiring-entity name (max 300 chars).
proposedEntityJurisdictionstring| nullOptionalProposed entity jurisdiction (max 100 chars).
pricenumber| nullOptionalPrice ≥ 0.
currencystring| nullOptionalCurrency code (max 10 chars). Defaults to GBP on create.
siteSizeobject| nullOptional{ value: number ≥ 0, unit: sqm | sqft | acre }.
depositobject| nullOptional{ kind: percent | amount, value: number ≥ 0 }.
purchaseConditionsarray<object>OptionalArray (max 300) of { id, label, met: boolean }.
targetsarray<object>OptionalArray (max 300) of milestones { id, label, kind: stage | custom, stage?, targetDate?, actualDate? (ISO dates) }.
partiesarray<object>OptionalArray (max 300) of { role (deal-party role id), name, companyId?, companyName?, personId?, entityId?, contact? }.
keyDatesobject| nullOptional{ hotSigned?, exclusivityEnds?, targetExchange?, exchanged?, targetCompletion?, completed?, planningDecisionExpectedDate? } — all ISO dates.
appraisalobject| nullOptional{ gdv?: number ≥ 0, totalCost?: number ≥ 0 }.
appraisalScenariosarray<object>OptionalArray (max 50) of { name, gdv?, totalCost?, holdingYears? }.
sourcestring| nullOptionalDeal source (max 300 chars).
regionMarketstring| nullOptionalRegion/market label (max 200 chars).
askingPricenumber| nullOptionalAsking price ≥ 0.
agreedPricenumber| nullOptionalAgreed price ≥ 0.
headlineMetricTypestring| nullOptionalHeadline metric type (max 100 chars).
headlineMetricValuestring| nullOptionalHeadline metric value (max 100 chars).
unitCountnumber| nullOptionalUnit count ≥ 0.
businessPlanSummarystring| nullOptionalBusiness-plan summary (max 5,000 chars).
keyRiskstring| nullOptionalKey risk (max 2,000 chars).
nextActionTextstring| nullOptionalNext action (max 500 chars).
nextActionDueDatestring| nullOptionalISO date.
hotsStatusstring| nullOptionalHoTs status id from HOTS_STATUS.
planningStatusstring| nullOptionalPlanning status id from PLANNING_STATUS.
currentPlanningStatusstring| nullOptionalCurrent planning status id from CURRENT_PLANNING_STATUS_LABELS.
creditTermsobject| nullOptional{ borrowerSponsor?, facilityType?, loanAmount?, ltv?, marginCoupon?, termMaturity?, seniority?, securityCollateral?, covenantSummary? }.
abortReasonstring| nullOptionalAbort reason (max 1,000 chars).
ddStatusstring| nullOptionalDue-diligence status id from DD_STATUS.
legalStatusstring| nullOptionalLegal status id from LEGAL_STATUS.
prioritystring| nullOptionalPriority id from DEAL_PRIORITY.
customFieldsobjectOptionalWorkspace-defined custom fields: string | number | boolean | null values, at most 100 keys.
dueDiligencearray<object>OptionalArray (max 300) of { id, label, done: boolean, category? }.
tasksarray<object>OptionalArray (max 300) of { id, title, dueDate? (ISO date), done: boolean }.
projectIdstring| nullOptionalLinked project id.
tagsarray<string>OptionalTag ids (max 100, unique).
colourstring| nullOptionalSix-digit hex colour.
notesstring| nullOptionalFree notes (max 10,000 chars).
stageobjectOptionalAccepted but IGNORED (service-layer-owned; silently stripped from the write).
lockedobjectOptionalAccepted but IGNORED (silently stripped).
icSubmissionobjectOptionalAccepted but IGNORED (silently stripped).
acquiringEntityIdobjectOptionalId of an existing company to set as the buyer, applied only while the deal has none (422 if the company fails the create checks or the deal is locked); otherwise IGNORED (silently stripped).
parentHoldcoIdobjectOptionalAccepted but IGNORED (silently stripped).
activityobjectOptionalAccepted but IGNORED (silently stripped).
foundationDesignIdobjectOptionalAccepted but IGNORED (silently stripped).

Response

200 — The merged deal record.

Headers: ETag

Errors (13)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X PATCH "https://app.alethiahq.com/api/v1/deals/deal_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "If-Match: 3" \
  -d '{
  "name": "example"
}'
DELETE/api/v1/deals/{dealId}Delete a deal

Deletes a deal that never left the desk. Fires deal.deleted. A deal that holds governance or evidence is never hard-deleted through the API: one ever submitted to the investment committee, locked, completed, linked to an acquiring entity or a Foundation plan, or with documents filed against it or a KYC case context for it. Such a request is refused with 409 record_retained and nothing is changed. The rule is the same as the in-app admin delete.

scope: write

Parameters

ParameterInTypeRequiredDescription
dealIdpathstringRequiredDeal document id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Response

200 — { deleted: true, id: string }

Errors (11)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
409record_retainedThis deal cannot be deleted: it has been submitted to the investment committee, is locked, completed or linked, or other records refer to it.The deal holds governance or evidence (see the description). The body does not say which. Nothing is changed.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X DELETE "https://app.alethiahq.com/api/v1/deals/deal_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "If-Match: 3"

Resource

Documents

Document metadata records. No module gate — documents belong to both persisted application modules (key scope + Enterprise are the authority). The API cannot upload bytes: created documents are metadata/link records and storagePath is always server-set to the empty string. Restricted documents are hidden from keys without restricted_records; setting restricted/viewerIds requires that capability.

GET/api/v1/documentsList documents

Lists documents with cursor paging. Records with restricted: true are filtered out unless the key has restricted_records.

scope: read

Parameters

ParameterInTypeRequiredDescription
limitqueryintegerOptionalPage size. Default 50, maximum 200; non-numeric or non-positive values fall back to the default.
cursorquerystringOptionalOpaque cursor from the previous page's nextCursor. Treat as opaque — for keys without restricted_records it is an encrypted token.

Response

200 — { data: Document[], nextCursor: string | null }. Each element: id, name, category, kind, source, storagePath (always "" for API-created records), externalUrl, contentType, sizeBytes, projectId, entityId, entityIds, agreementId, dealId, tags, archived, restricted, viewerIds, workspaceId, audit stamps — plus any app-written fields on documents created in-app.

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid pagination cursor.The cursor is malformed or was tampered with.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/documents" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
POST/api/v1/documentsRegister a document (metadata/link only)

Creates a metadata-only document record. category defaults to "other", kind to "uploaded", source to "link"; storagePath is forced to "". Setting restricted/viewerIds without the restricted_records capability → 403. Fires document.created.

scope: write

Request headers

Idempotency-Key
Optional client-chosen token, 8–200 chars, no control characters. The same key with the same request replays the original 2xx response (Idempotency-Replayed: true) without repeating the write; the same key with a different request → 409. Keys are scoped to the API key, remembered for 24 hours, and only a successful response is stored (a failed request frees the key). Malformed → 400.

Request body

Strict object validated by PublicApiDocumentCreateSchema. storagePath, signing, sharePoint, currentVersionId, version, uploadedAt/uploadedBy are server-owned and rejected as unknown keys.

FieldTypeRequiredDescription
namestringRequiredDocument name, 1–300 chars.
categorystringOptionalDefaults to "other".Allowed: "incorporation", "constitutional", "registers", "accounts", "tax", "board_minutes", "resolutions", "agreement", "filings", "kyc", "banking", "other"
kindstringOptionalDefaults to "uploaded".Allowed: "uploaded", "generated"
sourcestringOptionalDefaults to "link". The API cannot upload bytes regardless of this value.Allowed: "upload", "sharepoint", "link"
externalUrlstringOptionalLink target URL (max 2,048 chars).
contentTypestringOptionalMIME type (max 200 chars).
sizeBytesnumberOptionalSize in bytes ≥ 0.
projectIdstringOptionalLinked project id.
entityIdstringOptionalPrimary linked entity id.
entityIdsarray<string>OptionalLinked entity ids (max 200, unique).
agreementIdstringOptionalLinked agreement id.
dealIdstringOptionalLinked deal id.
tagsarray<string>OptionalTag ids (max 100, unique).
archivedbooleanOptionalArchive flag.
restrictedbooleanOptionalRecord-level ACL flag — requires restricted_records.
viewerIdsarray<string>OptionalExplicit viewer ids (max 500) — requires restricted_records.

Response

201 — The created document record (id, fields above, storagePath: "", workspaceId, audit stamps, createdVia: "api").

Headers: Idempotency-Replayed, ETag

Errors (13)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
403forbiddenForbidden: The restricted_records capability is required to change record access.restricted or viewerIds supplied without the capability.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIdempotency-Key must be 8-200 characters with no control characters or surrounding whitespace.An Idempotency-Key header is present but malformed.
409conflictIdempotency key conflicts with an earlier request. | A request with this Idempotency-Key is still being processed.The key was already used with a different request, or the first request with it is still running (Retry-After is set).

Example request

curl -X POST "https://app.alethiahq.com/api/v1/documents" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f2d6b7a-9d61-4e3a-8c2a-5e7b1f0a6c9d" \
  -d '{
  "name": "example"
}'
GET/api/v1/documents/{docId}Get a document

Reads one document record. Restricted records the key cannot access return 404.

scope: read

Parameters

ParameterInTypeRequiredDescription
docIdpathstringRequiredDocument id.

Response

200 — The document record.

Headers: ETag

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/documents/doc_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
PATCH/api/v1/documents/{docId}Update a document

Merge-patches a document (same fields as create, nullable where clearable). Changing restricted/viewerIds requires restricted_records (403). The category of retained evidence cannot be changed other than to "kyc": a KYC document (category "kyc"), a document linked to a person, and a document that is KYC or compliance evidence by its recorded provenance or evidence-only purpose. Such a request is refused with 409 document_retained and nothing is changed; archive the document instead (archived: true). Fires document.updated.

scope: write

Parameters

ParameterInTypeRequiredDescription
docIdpathstringRequiredDocument id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Request body

Strict object, at least one field (PublicApiDocumentPatchSchema). Same field set as create; externalUrl, contentType, sizeBytes, projectId, entityId, agreementId, dealId accept null to clear.

FieldTypeRequiredDescription
namestringOptionalDocument name, 1–300 chars.
categorystringOptionalCategory.Allowed: "incorporation", "constitutional", "registers", "accounts", "tax", "board_minutes", "resolutions", "agreement", "filings", "kyc", "banking", "other"
kindstringOptionalKind.Allowed: "uploaded", "generated"
sourcestringOptionalSource.Allowed: "upload", "sharepoint", "link"
externalUrlstring| nullOptionalLink URL; null clears.
contentTypestring| nullOptionalMIME type; null clears.
sizeBytesnumber| nullOptionalSize in bytes; null clears.
projectIdstring| nullOptionalProject id; null clears.
entityIdstring| nullOptionalEntity id; null clears.
entityIdsarray<string>OptionalLinked entity ids (max 200).
agreementIdstring| nullOptionalAgreement id; null clears.
dealIdstring| nullOptionalDeal id; null clears.
tagsarray<string>OptionalTag ids (max 100).
archivedbooleanOptionalArchive flag.
restrictedbooleanOptionalRequires restricted_records.
viewerIdsarray<string>OptionalRequires restricted_records.

Response

200 — The merged document record.

Headers: ETag

Errors (15)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
403forbiddenForbidden: The restricted_records capability is required to change record access.restricted or viewerIds supplied without the capability.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
409document_retainedThe category of retained evidence (KYC or compliance evidence, or a document linked to a person) can only be changed to kyc. Archive the document instead (PATCH archived: true).The stored document is retained evidence (category "kyc", linked to a person, or KYC or compliance evidence) and the request sets a category other than kyc. Nothing is changed; archive the document instead.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X PATCH "https://app.alethiahq.com/api/v1/documents/doc_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "If-Match: 3" \
  -d '{
  "name": "example"
}'
DELETE/api/v1/documents/{docId}Delete a document

Deletes a document record (metadata only). Fires document.deleted. KYC documents (category "kyc"), documents linked to a person, and documents that are KYC or compliance evidence by their recorded provenance or evidence-only purpose are retained evidence and are never hard-deleted through the API: the request is refused with 409 document_retained and nothing is changed; archive the document instead (PATCH archived: true). A document still cited by a meeting or resolution is refused with 409 conflict and the citing ids.

scope: write

Parameters

ParameterInTypeRequiredDescription
docIdpathstringRequiredDocument id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Response

200 — { deleted: true, id: string }

Errors (12)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
409document_retainedThis document is retained evidence (KYC or compliance evidence, or linked to a person) and cannot be deleted. Archive it instead (PATCH archived: true).The document has category "kyc", is linked to one or more people, or is KYC or compliance evidence. Nothing is changed; archive the document instead.
409conflictDocument is still referenced by governance records; archive it instead of deleting.A meeting (filed or linked) or a resolution still cites the document. The body adds references: [{ collection, ids }]. Nothing is changed.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X DELETE "https://app.alethiahq.com/api/v1/documents/doc_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "If-Match: 3"

Resource

Properties

Real-estate object layer (PropCo/AssetCo holdings). READ-ONLY over the API — the routes export GET only. A key without the restricted_records capability gets no record here that belongs to or names a restricted entity, or an entity that does not exist (for a shareholding: its issuer and an entity holder; for a deal: its buying company and the entities its parties act for). Such a record is absent from lists and answers 404 on GET (and on PATCH and DELETE where the resource offers them), exactly as a missing record does; where the resource offers create or update, naming such an entity answers 422, exactly as naming a missing one does.

GET/api/v1/propertiesList properties

Lists property records with cursor paging. Raw stored documents — the API applies no reshaping.

scope: read

Parameters

ParameterInTypeRequiredDescription
limitqueryintegerOptionalPage size. Default 50, maximum 200; non-numeric or non-positive values fall back to the default.
cursorquerystringOptionalOpaque cursor from the previous page's nextCursor. Treat as opaque — for keys without restricted_records it is an encrypted token.

Response

200 — { data: Property[], nextCursor: string | null }. Each element (Property type): id, workspaceId, name, entityId, titleNumber, tenure, useClass, address, status, acquisitionDate, acquisitionPrice, acquisitionCurrency, valuation, leases, notes, audit stamps.

Errors (7)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/properties" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
GET/api/v1/properties/{propertyId}Get a property

Reads one property record.

scope: read

Parameters

ParameterInTypeRequiredDescription
propertyIdpathstringRequiredProperty document id.

Response

200 — The property record (see list response).

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/properties/property_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"

Resource

Fund commitments

LP capital-account commitments into fund vehicles. READ-ONLY over the API (list only; no single-record route exists). In the app these records are access-controlled per FUND — each commitment carries the viewer list mirrored from its fund entity — but an API key is not a member and appears on no fund's viewer list, so the whole resource requires the restricted_records capability: a key without it reads NOTHING here (403, never an empty list), and a key with it reads the workspace's ENTIRE register with no per-fund narrowing. Grant that capability only to a key you would give the whole LP register to.

GET/api/v1/commitmentsList fund commitments

Lists commitment records with cursor paging. Raw stored documents — no reshaping. Requires the restricted_records capability outright: without it the request is refused (403) rather than filtered, because a commitment that carries no viewer list yet is admin-only, not open. With it, every commitment in the workspace is returned regardless of which fund it belongs to.

scope: readcapability: restricted_records

Parameters

ParameterInTypeRequiredDescription
limitqueryintegerOptionalPage size. Default 50, maximum 200; non-numeric or non-positive values fall back to the default.
cursorquerystringOptionalOpaque cursor from the previous page's nextCursor. Treat as opaque — for keys without restricted_records it is an encrypted token.

Response

200 — { data: FundCommitment[], nextCursor: string | null }. Each element (FundCommitment type): id, workspaceId, fundEntityId, investorName (the LP's name, as entered), investorId and investorType (person | entity) only when the LP is linked to an existing record, amountMinor (exact minor units as a decimal string; older rows carry a numeric amount in major units instead), currency, fundPartId (the separate fund, when the fund has them), commitmentDate, flows (capital calls/distributions), restricted/viewerIds (the fund's mirrored ACL), audit stamps.

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
403forbiddenForbidden: restricted_records capability requiredThe key does not carry the restricted_records capability. The LP register has no open records, so nothing is returned rather than a filtered page.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/commitments" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"

Resource

Cap table — Share classes

Share classes issued by an entity. entityId is checked to exist in the key's workspace on create and is immutable afterwards (a PATCH attempt is rejected as an unknown key). A key without the restricted_records capability gets no record here that belongs to or names a restricted entity, or an entity that does not exist (for a shareholding: its issuer and an entity holder; for a deal: its buying company and the entities its parties act for). Such a record is absent from lists and answers 404 on GET (and on PATCH and DELETE where the resource offers them), exactly as a missing record does; where the resource offers create or update, naming such an entity answers 422, exactly as naming a missing one does.

GET/api/v1/cap-table/share-classesList share classes

Lists share classes with cursor paging.

scope: read

Parameters

ParameterInTypeRequiredDescription
limitqueryintegerOptionalPage size. Default 50, maximum 200; non-numeric or non-positive values fall back to the default.
cursorquerystringOptionalOpaque cursor from the previous page's nextCursor. Treat as opaque — for keys without restricted_records it is an encrypted token.

Response

200 — { data: ShareClass[], nextCursor: string | null }. Each element: id plus the create fields below, workspaceId and audit stamps.

Errors (7)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/cap-table/share-classes" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
POST/api/v1/cap-table/share-classesCreate a share class

Creates a share class. entityId must reference an entity in the key's workspace (422 otherwise). votingRight defaults to "ordinary". Fires shareClass.created.

scope: write

Request headers

Idempotency-Key
Optional client-chosen token, 8–200 chars, no control characters. The same key with the same request replays the original 2xx response (Idempotency-Replayed: true) without repeating the write; the same key with a different request → 409. Keys are scoped to the API key, remembered for 24 hours, and only a successful response is stored (a failed request frees the key). Malformed → 400.

Request body

Strict object validated by PublicApiShareClassCreateSchema.

FieldTypeRequiredDescription
entityIdstringRequiredIssuing entity id — must exist in this workspace.
namestringRequiredClass name, 1–200 chars.
votingRightstringOptionalDefaults to "ordinary".Allowed: "ordinary", "non_voting", "multiple", "preference"
votesPerSharenumberOptionalVotes per share > 0.
parValuenumberOptionalPar value ≥ 0.
noParValuebooleanOptionalNo-par-value flag.
statedCapitalnumberOptionalStated capital ≥ 0.
currencystringOptionalCurrency code (max 10 chars).
authorisedSharesnumberOptionalAuthorised shares ≥ 0.
rightsobjectOptional{ preferredDividend?, liquidationPreference?, participating?, antiDilution? } — all booleans.
liquidationTermsobjectOptional{ liquidationMultiple? ≥ 0, participationCap? ≥ 0, seniority?: number }.
descriptionstringOptionalMax 2,000 chars.

Response

201 — The created share-class record.

Headers: Idempotency-Replayed, ETag

Errors (12)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIdempotency-Key must be 8-200 characters with no control characters or surrounding whitespace.An Idempotency-Key header is present but malformed.
409conflictIdempotency key conflicts with an earlier request. | A request with this Idempotency-Key is still being processed.The key was already used with a different request, or the first request with it is still running (Retry-After is set).

Example request

curl -X POST "https://app.alethiahq.com/api/v1/cap-table/share-classes" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f2d6b7a-9d61-4e3a-8c2a-5e7b1f0a6c9d" \
  -d '{
  "entityId": "entity_123",
  "name": "example"
}'
GET/api/v1/cap-table/share-classes/{id}Get a share class

Reads one share class.

scope: read

Parameters

ParameterInTypeRequiredDescription
idpathstringRequiredShare-class document id.

Response

200 — The share-class record.

Headers: ETag

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/cap-table/share-classes/id_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
PATCH/api/v1/cap-table/share-classes/{id}Update a share class

Merge-patches a share class. entityId is immutable (unknown key → 422). Numeric/text/object fields accept null to clear. Fires shareClass.updated.

scope: write

Parameters

ParameterInTypeRequiredDescription
idpathstringRequiredShare-class document id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Request body

Strict object, at least one field (PublicApiShareClassPatchSchema).

FieldTypeRequiredDescription
namestringOptionalClass name, 1–200 chars.
votingRightstringOptionalVoting right.Allowed: "ordinary", "non_voting", "multiple", "preference"
votesPerSharenumber| nullOptionalVotes per share > 0; null clears.
parValuenumber| nullOptionalPar value ≥ 0; null clears.
noParValuebooleanOptionalNo-par-value flag.
statedCapitalnumber| nullOptionalStated capital; null clears.
currencystring| nullOptionalCurrency code; null clears.
authorisedSharesnumber| nullOptionalAuthorised shares; null clears.
rightsobject| nullOptionalRights object; null clears.
liquidationTermsobject| nullOptionalLiquidation terms; null clears.
descriptionstring| nullOptionalMax 2,000 chars; null clears.

Response

200 — The merged share-class record.

Headers: ETag

Errors (13)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X PATCH "https://app.alethiahq.com/api/v1/cap-table/share-classes/id_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "If-Match: 3" \
  -d '{
  "name": "example"
}'
DELETE/api/v1/cap-table/share-classes/{id}Delete a share class

Deletes a share class. Fires shareClass.deleted.

scope: write

Parameters

ParameterInTypeRequiredDescription
idpathstringRequiredShare-class document id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Response

200 — { deleted: true, id: string }

Errors (10)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X DELETE "https://app.alethiahq.com/api/v1/cap-table/share-classes/id_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "If-Match: 3"

Resource

Cap table — Shareholdings

Holdings of a share class by an entity or person. entityId and shareClassId are referentially checked on create; entityId, shareClassId, holderId and holderType are immutable afterwards — a share transfer is a NEW shareholding, mirroring the internal transaction ledger. A key without the restricted_records capability gets no record here that belongs to or names a restricted entity, or an entity that does not exist (for a shareholding: its issuer and an entity holder; for a deal: its buying company and the entities its parties act for). Such a record is absent from lists and answers 404 on GET (and on PATCH and DELETE where the resource offers them), exactly as a missing record does; where the resource offers create or update, naming such an entity answers 422, exactly as naming a missing one does.

GET/api/v1/cap-table/shareholdingsList shareholdings

Lists shareholdings with cursor paging.

scope: read

Parameters

ParameterInTypeRequiredDescription
limitqueryintegerOptionalPage size. Default 50, maximum 200; non-numeric or non-positive values fall back to the default.
cursorquerystringOptionalOpaque cursor from the previous page's nextCursor. Treat as opaque — for keys without restricted_records it is an encrypted token.

Response

200 — { data: Shareholding[], nextCursor: string | null }. Each element: id plus the create fields below, workspaceId and audit stamps.

Errors (7)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/cap-table/shareholdings" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
POST/api/v1/cap-table/shareholdingsCreate a shareholding

Creates a shareholding. entityId and shareClassId must exist in the key's workspace (422 with per-field messages otherwise). status defaults to "current" so the holding is visible to cap-table builds. Fires shareholding.created.

scope: write

Request headers

Idempotency-Key
Optional client-chosen token, 8–200 chars, no control characters. The same key with the same request replays the original 2xx response (Idempotency-Replayed: true) without repeating the write; the same key with a different request → 409. Keys are scoped to the API key, remembered for 24 hours, and only a successful response is stored (a failed request frees the key). Malformed → 400.

Request body

Strict object validated by PublicApiShareholdingCreateSchema.

FieldTypeRequiredDescription
entityIdstringRequiredIssuing entity id — must exist in this workspace.
shareClassIdstringRequiredShare-class id — must exist in this workspace.
holderIdstringRequiredHolder id (entity or person document id).
holderTypestringRequiredWhich collection holderId references.Allowed: "entity", "person"
sharesnumberRequiredShare count ≥ 0.
paidUpbooleanOptionalPaid-up flag.
amountUnpaidnumberOptionalUnpaid amount ≥ 0.
encumbrancestringOptionalEncumbrance.Allowed: "none", "pledge", "usufruct"
statusstringOptionalDefaults to "current".Allowed: "current", "historic"

Response

201 — The created shareholding record.

Headers: Idempotency-Replayed, ETag

Errors (12)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIdempotency-Key must be 8-200 characters with no control characters or surrounding whitespace.An Idempotency-Key header is present but malformed.
409conflictIdempotency key conflicts with an earlier request. | A request with this Idempotency-Key is still being processed.The key was already used with a different request, or the first request with it is still running (Retry-After is set).

Example request

curl -X POST "https://app.alethiahq.com/api/v1/cap-table/shareholdings" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f2d6b7a-9d61-4e3a-8c2a-5e7b1f0a6c9d" \
  -d '{
  "entityId": "entity_123",
  "shareClassId": "shareclass_123",
  "holderId": "holder_123",
  "holderType": "entity",
  "shares": 1
}'
GET/api/v1/cap-table/shareholdings/{id}Get a shareholding

Reads one shareholding.

scope: read

Parameters

ParameterInTypeRequiredDescription
idpathstringRequiredShareholding document id.

Response

200 — The shareholding record.

Headers: ETag

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/cap-table/shareholdings/id_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
PATCH/api/v1/cap-table/shareholdings/{id}Update a shareholding

Merge-patches a shareholding. entityId/shareClassId/holderId/holderType are immutable (unknown key → 422). Fires shareholding.updated.

scope: write

Parameters

ParameterInTypeRequiredDescription
idpathstringRequiredShareholding document id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Request body

Strict object, at least one field (PublicApiShareholdingPatchSchema).

FieldTypeRequiredDescription
sharesnumberOptionalShare count ≥ 0.
paidUpbooleanOptionalPaid-up flag.
amountUnpaidnumber| nullOptionalUnpaid amount; null clears.
encumbrancestring| nullOptionalEncumbrance; null clears.Allowed: "none", "pledge", "usufruct"
statusstringOptionalHolding status.Allowed: "current", "historic"

Response

200 — The merged shareholding record.

Headers: ETag

Errors (13)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X PATCH "https://app.alethiahq.com/api/v1/cap-table/shareholdings/id_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "If-Match: 3" \
  -d '{
  "shares": 1
}'
DELETE/api/v1/cap-table/shareholdings/{id}Delete a shareholding

Deletes a shareholding. Fires shareholding.deleted. A shareholding held by a person is part of the register of members and is never hard-deleted through the API: the request is refused with 409 record_retained and nothing is changed; mark it historic instead (PATCH status: "historic").

scope: write

Parameters

ParameterInTypeRequiredDescription
idpathstringRequiredShareholding document id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Response

200 — { deleted: true, id: string }

Errors (11)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
409record_retainedThis shareholding is held by a person and is part of the register of members, so it cannot be deleted. Mark it historic instead (PATCH status: historic).The shareholding's holderType is person. Nothing is changed; mark it historic instead.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X DELETE "https://app.alethiahq.com/api/v1/cap-table/shareholdings/id_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "If-Match: 3"

Resource

Webhook subscriptions

Manage outbound webhook subscriptions. Every operation requires the manage_webhooks capability on top of the read/write scope; mutations additionally require the workspace's Enterprise webhook entitlement. A workspace may hold at most 25 subscriptions. The signingSecret is returned exactly once, in the first successful POST response (never in a replay, a list or a read); there is no rotate endpoint, so a lost secret means deleting the subscription and creating a new one.

GET/api/v1/webhooksList webhook subscriptions

Lists all subscriptions (no paging — the collection is capped at 25).

scope: readcapability: manage_webhooks

Response

200 — { data: WebhookSubscription[] } (no nextCursor). Each element: { id, url, events, recordAccess (open_only | restricted), enabled, createdAt?, updatedAt?, lastDeliveryAt?, lastDeliveryStatus? (success | failure) } — signingSecret is NEVER included after creation.

Errors (8)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
403forbiddenForbidden: manage_webhooks capability requiredThe key does not carry the manage_webhooks capability.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/webhooks" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
POST/api/v1/webhooksCreate a webhook subscription

Registers an HTTPS endpoint. The URL must be public HTTPS (private/loopback/link-local/metadata hosts, credentials, fragments are rejected — SSRF guard), at most 2,048 chars. recordAccess: "restricted" requires the restricted_records capability. The response is the ONLY time signingSecret (whsec_ + 48 hex chars) is revealed, and it carries signingSecretShown: true. The secret is never stored for replay: repeating the request with the same Idempotency-Key returns the same subscription with signingSecret omitted and signingSecretShown: false. If you lost the secret there is no rotate or regenerate endpoint: DELETE the subscription and create a new one (with a new Idempotency-Key) to get a fresh secret.

scope: writecapability: manage_webhooks

Request headers

Idempotency-Key
Optional client-chosen token, 8–200 chars, no control characters. The same key with the same request replays the original 2xx response (Idempotency-Replayed: true) without repeating the write; the same key with a different request → 409. Keys are scoped to the API key, remembered for 24 hours, and only a successful response is stored (a failed request frees the key). Malformed → 400.

Request body

Strict object — only url, events and recordAccess are allowed.

FieldTypeRequiredDescription
urlstringRequiredPublic HTTPS endpoint, max 2,048 chars, no credentials or fragment.
eventsarray<string>RequiredNon-empty array of subscribable event names (see webhooks.events), no duplicates. "ping" cannot be subscribed.
recordAccessstringOptionalDefaults to "open_only". "restricted" requires the restricted_records capability.Allowed: "open_only", "restricted"

Response

201 — { id, url, events, recordAccess, enabled: true, signingSecret (shown ONCE, first response only), signingSecretShown: true, createdAt }. An Idempotency-Key replay returns the same body with signingSecret omitted and signingSecretShown: false.

Headers: Idempotency-Replayed, ETag

Errors (14)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
403forbiddenForbidden: manage_webhooks capability requiredThe key does not carry the manage_webhooks capability.
403forbiddenOutbound webhooks require Enterprise.The workspace billing document is not an active/trialing Enterprise subscription.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedUnknown keys, bad URL, unknown/duplicate events, or recordAccess: restricted without the capability — fields map names each.
409conflictA workspace may have at most 25 webhook subscriptions.The 25-subscription cap is reached. Body carries limit: 25.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIdempotency-Key must be 8-200 characters with no control characters or surrounding whitespace.An Idempotency-Key header is present but malformed.
409conflictIdempotency key conflicts with an earlier request. | A request with this Idempotency-Key is still being processed.The key was already used with a different request, or the first request with it is still running (Retry-After is set).

Example request

curl -X POST "https://app.alethiahq.com/api/v1/webhooks" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f2d6b7a-9d61-4e3a-8c2a-5e7b1f0a6c9d" \
  -d '{
  "url": "https://example.com/hooks/alethia",
  "events": [
    "deal.created",
    "deal.updated"
  ]
}'
GET/api/v1/webhooks/{subId}Get a webhook subscription

Reads one subscription (strict public projection — secrets and internal principals never returned).

scope: readcapability: manage_webhooks

Parameters

ParameterInTypeRequiredDescription
subIdpathstringRequiredSubscription id.

Response

200 — { id, url, events, recordAccess (open_only | restricted), enabled, createdAt?, updatedAt?, lastDeliveryAt?, lastDeliveryStatus? (success | failure) } — signingSecret is NEVER included after creation.

Headers: ETag

Errors (9)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
403forbiddenForbidden: manage_webhooks capability requiredThe key does not carry the manage_webhooks capability.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Example request

curl "https://app.alethiahq.com/api/v1/webhooks/sub_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"
PATCH/api/v1/webhooks/{subId}Update a webhook subscription

Strictly permits enabled, url, events and recordAccess; anything else → 422. At least one updatable field must be supplied.

scope: writecapability: manage_webhooks

Parameters

ParameterInTypeRequiredDescription
subIdpathstringRequiredSubscription id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Request body

Strict partial update.

FieldTypeRequiredDescription
enabledbooleanOptionalEnable/disable delivery.
urlstringOptionalNew public HTTPS endpoint (same validation as create).
eventsarray<string>OptionalReplacement event list (same validation as create).
recordAccessstringOptional"restricted" requires the restricted_records capability.Allowed: "open_only", "restricted"

Response

200 — The updated subscription projection.

Headers: ETag

Errors (14)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
403forbiddenForbidden: manage_webhooks capability requiredThe key does not carry the manage_webhooks capability.
403forbiddenOutbound webhooks require Enterprise.The workspace billing document is not an active/trialing Enterprise subscription.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
422invalid_requestValidation failedUnknown keys, no updatable fields, or a field fails validation.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X PATCH "https://app.alethiahq.com/api/v1/webhooks/sub_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Content-Type: application/json" \
  -H "If-Match: 3" \
  -d '{
  "enabled": true
}'
DELETE/api/v1/webhooks/{subId}Delete a webhook subscription

Removes the subscription. Deleting immediately frees capacity against the 25-subscription cap.

scope: writecapability: manage_webhooks

Parameters

ParameterInTypeRequiredDescription
subIdpathstringRequiredSubscription id.

Request headers

If-Match
Optional. An ETag from a previous GET/PATCH of this record (quoted), or *. When the record has changed since → 412 with the current ETag; omit it for an unconditional write. Malformed → 400.

Response

200 — { deleted: true, id: string }

Errors (11)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
403forbiddenForbidden: manage_webhooks capability requiredThe key does not carry the manage_webhooks capability.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.

Example request

curl -X DELETE "https://app.alethiahq.com/api/v1/webhooks/sub_123" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "If-Match: 3"
POST/api/v1/webhooks/{subId}/testSend a test (ping) delivery

Fires a synthetic "ping" event at the subscription's URL, signed with its signingSecret, and reports the outcome. The subscription must be enabled and structurally deliverable. Payload data: { message: "Test delivery from Alethia", subscriptionId }.

scope: writecapability: manage_webhooks

Parameters

ParameterInTypeRequiredDescription
subIdpathstringRequiredSubscription id.

Request headers

Idempotency-Key
Optional client-chosen token, 8–200 chars, no control characters. The same key with the same request replays the original 2xx response (Idempotency-Replayed: true) without repeating the write; the same key with a different request → 409. Keys are scoped to the API key, remembered for 24 hours, and only a successful response is stored (a failed request frees the key). Malformed → 400.

Response

200 — { ok: boolean, statusCode: number (0 on network failure), durationMs: number, skipped?: "ssrf" }

Headers: Idempotency-Replayed

Errors (13)
StatusCodeErrorWhen
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
403forbiddenForbidden: manage_webhooks capability requiredThe key does not carry the manage_webhooks capability.
403forbiddenOutbound webhooks require Enterprise.The workspace billing document is not an active/trialing Enterprise subscription.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
409conflictWebhook subscription is not deliverable.The stored subscription fails integrity validation (disabled, malformed URL/events/secret, or wrong workspace).
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.
400invalid_requestIdempotency-Key must be 8-200 characters with no control characters or surrounding whitespace.An Idempotency-Key header is present but malformed.
409conflictIdempotency key conflicts with an earlier request. | A request with this Idempotency-Key is still being processed.The key was already used with a different request, or the first request with it is still running (Retry-After is set).

Example request

curl -X POST "https://app.alethiahq.com/api/v1/webhooks/sub_123/test" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}" \
  -H "Idempotency-Key: 9f2d6b7a-9d61-4e3a-8c2a-5e7b1f0a6c9d"

Talk to us about your integration.

Tell us what you are building and we will walk through the resources and events that fit.