Public API surface
This page records the public contract in the current source tree and the production hosts that serve it. Source changes can precede deployment, so verify /health and /api/capabilities before assuming a newly documented capability is live. Routes reserved for internal service-to-service traffic, administration, or unsupported compatibility shims are not listed unless their public migration response is itself part of the contract.
Source-contract review (21 September 2026). The hosts below are the intended public surfaces. A source change can precede its production deployment, so the current release still requires exact-build health and smoke receipts. Several data surfaces may be empty —
GET /api/anchorscan return{"items": [], "count": 0}andGET /api/epoch/currentcan return{"epoch": null, "message": "No active epoch"}. Where a capability is scaffolded rather than in service, this page says so.
Hosts
| Host | Service | Scope | Version label (verify live build separately) |
|---|---|---|---|
api.ma-atara.io | core-worker | Chain, notes, inbox, inception, anchors, assets, governance, pairing, PQC verification | 1.5.2 |
aegis.ma-atara.io | aegis-worker | Provenance registration, verification, similarity search, client assets | 0.2.3 |
consent.ma-atara.io | ai-consent-worker | AI-training consent discovery and lookup | 1.0.3 |
did.ma-atara.io | did-resolver-worker | DID resolution and registration, org bindings | 0.1.0 |
keys.ma-atara.io | edge-key-fabric | DID-native key directory, prekeys, guardian consensus, epochs, transparency | 0.1.5 |
auth.ma-atara.io | maatara-auth-worker | Human sign-in (Turnstile, OPAQUE, passkeys, OAuth onboarding) | 0.1.4 |
pay.ma-atara.io | payments-worker | Pricing, entitlements, quota | 0.1.3 |
docs.ma-atara.io | docs-worker | This documentation site | 1.0.1 |
Aliases: aegis.ma-atara.io is also served at provenance.ma-atara.io and api.maatara.io; consent.ma-atara.io is also served at consent.maatara.io. All five hostnames are live — the first three reach aegis-worker, the last two ai-consent-worker. Note the near-collision: api.maatara.io (unhyphenated) is aegis, not the core worker at api.ma-atara.io. Use the hyphenated ma-atara.io names in new integrations.
Deployment tooling injects build identity. Record the service and build reported by /health and /api/version; a value of unknown is missing evidence, not a deployment identifier. A version label alone does not distinguish different builds of the same service version.
Try it now
The read-only liveness and discovery commands below need no credentials or account. Mutation examples are intentionally omitted from this smoke list because some production mutations require an allow-listed Origin, a scoped assertion, or both. Placeholder lookups may answer with data or a 404; do not infer that a mutation is unauthenticated from a read response.
# Liveness across the fleet
curl https://api.ma-atara.io/health
curl https://aegis.ma-atara.io/health
curl https://consent.ma-atara.io/health
curl https://did.ma-atara.io/health
curl https://keys.ma-atara.io/health
curl https://auth.ma-atara.io/health
curl https://pay.ma-atara.io/health
curl https://docs.ma-atara.io/health
# Capability and version discovery
curl https://api.ma-atara.io/api/capabilities
curl https://api.ma-atara.io/api/version
curl https://api.ma-atara.io/openapi.json
curl https://aegis.ma-atara.io/api/capabilities
curl https://keys.ma-atara.io/api/capabilities
# DID resolution (W3C DID Core / Universal Resolver shaped)
curl https://did.ma-atara.io/.well-known/did.json
curl -H 'Accept: application/did-resolution' "https://did.ma-atara.io/1.0/identifiers/did:maatara:<fingerprint>"
curl "https://did.ma-atara.io/1.0/org/<orgDid>/officers"
curl "https://did.ma-atara.io/1.0/org/<orgDid>/verify/<memberDid>"
# AI-training consent (crawler-facing)
curl https://consent.ma-atara.io/robots.txt
curl https://consent.ma-atara.io/.well-known/ai-consent
curl https://consent.ma-atara.io/.well-known/tdmrep.json
curl https://consent.ma-atara.io/api/v1/consent/<contentHash>
# Anchors and epochs
curl "https://api.ma-atara.io/api/anchors?limit=10"
curl "https://api.ma-atara.io/api/anchors/state-proof?window=256"
curl https://aegis.ma-atara.io/api/anchor/status
curl https://keys.ma-atara.io/api/epoch/current
# Pricing, region-resolved from the caller's IP
curl https://pay.ma-atara.io/pay/offer
curl https://pay.ma-atara.io/pay/offer/provenance_yearly
curl "https://pay.ma-atara.io/pay/offer?country=GB"
aegis.ma-atara.io/health is a dependency roll-up and returns 503 if any dependency check fails, so treat a non-200 there as a real signal rather than a probe error.
Authentication
The platform auth scheme is a stateless signed assertion:
Authorization: MaataraAssertion <assertionB64u>.<signatureB64u>
The signature algorithm is ML-DSA-65 (FIPS 204). Assertion construction, the mandatory nonce, and the canonical-DID binding rules are covered in Authentication; this page only records which endpoints require an assertion and what each service does with the aud claim.
Audience
aud names the service or route group the assertion may be presented to. Audience enforcement is fail-closed at each protected native boundary; there is no native unscoped or wrong-product fallback. The complete wire namespace is maintained in packages/identity-types/src/audiences.ts and includes product, worker, and Core route-group audiences.
| Service | Endpoint(s) | Required aud | If it does not match |
|---|---|---|---|
api.ma-atara.io | Core route groups: /api/notes/, /api/genesis/ and /api/inception/, /api/pair/, and the remaining protected chain/account routes | notes, genesis, core, or chain respectively | 401/403. The route's exact audience is required before nonce consumption or storage access |
did.ma-atara.io | POST /1.0/register; other protected resolver routes | identity for registration; identity or console for the route | 403 invalid_audience or an equivalent authorization failure |
keys.ma-atara.io | assertion-authenticated routes | keys | 401 Invalid assertion; a wrong or absent audience is not accepted |
aegis.ma-atara.io | assertion-authenticated registration, search and text-query routes | provenance | 403 wrong_audience; it is not downgraded to the anonymous tier |
pay.ma-atara.io | assertion-authenticated quota and entitlement routes | provenance | 403 wrong_audience; billing state is not read |
parable-city | protected identity and unlock routes | city | 403 wrong_audience |
identity-bridge | protected work-identity mutation routes | identity-bridge | 403 wrong_audience |
Public reads and route-specific credentials are separate contracts and do not imply acceptance of an arbitrary MaataraAssertion. Set the exact audience for the route and mint a fresh nonce; with several products sharing one identity across separate origins, reusing a token across services is rejected or replay-protected.
Some services also constrain the nonce beyond the general "non-empty and unique" rule: POST /1.0/register on did.ma-atara.io requires it to match ^[A-Za-z0-9._~-]{16,64}$ (400 invalid_nonce), and the device-directory and key-transparency routes on keys.ma-atara.io — which are not listed on this page — cap it at 128 characters (403 invalid_directory_nonce).
On api.ma-atara.io the nonce is consumed on every assertion check, including reads. An assertion is single-use even on GET /api/chain/head. Mint a fresh one per request.
Signature algorithm labels on the wire
The native profile uses ML-DSA-65 (FIPS 204). Native endpoints accept one exact label and do not perform historical algorithm or shape fallback:
| Endpoint | Field | Accepted values | Behaviour on anything else |
|---|---|---|---|
POST /api/pqc/verify (core) | alg | ml-dsa-65 only | Returns 200 {"valid": false} for an unsupported or omitted label |
POST /api/verify (aegis) | algorithm | ml-dsa-65 only | 400 with an algorithm error |
POST /api/register and /api/register/video (aegis) | signature.algorithm | ml-dsa-65 only | 400; a legacy label cannot create a new provenance record |
Temporary legacy account healing is retired (HTTP 410); it is not an available recovery service or native trust-path fallback. New Core chain/governance writes and Aegis registrations must emit ml-dsa-65.
Rate limits
Limits are per 60-second window. The shared limiter is keyed on CF-Connecting-IP; the only place the platform meters by identity rather than address is aegis similarity search.
Anonymous vs authenticated
This split exists on POST /api/search/* and POST /api/query/text on aegis.ma-atara.io:
| Tier | Limit | Key | How you qualify |
|---|---|---|---|
| anonymous | 10 / 60 s | client IP | Default — no assertion, an unverifiable assertion, an assertion with no aud, or an assertion with any aud other than provenance |
| authenticated | 60 / 60 s | caller's DID | A verifying MaataraAssertion carrying aud: "provenance" |
Both tiers return X-RateLimit-Limit and X-RateLimit-Tier on success. On rejection:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Tier: anonymous
{"success":false,"error":"Rate limit exceeded","requestId":"aegis-…","tier":"anonymous","retryAfter":60}
Shared per-IP tiers
Every other route is classified by path into one of these tiers:
| Tier | Limit / 60 s | Applies to |
|---|---|---|
health | 600 | /health, /api/capabilities |
verify | 500 | paths starting with /api/verify — not other paths that merely contain /verify, so POST /api/pqc/verify and GET /api/capsule/{id}/verify are _not_ in this tier |
read | 300 | any other GET |
default | 200 | everything else |
verify-batch | 100 | a path starting with /api/verify that also contains batch |
ai-search | 60 | paths containing /search |
ai-chat | 30 | paths containing /chat — no public route on these hosts matches today |
identity | 30 | paths containing /identity |
batch-sync | 30 | /sync/batch |
register | 20 | paths containing /register or /commitment |
epoch | 20 | paths containing /epoch |
key-rotate | 10 | /key/rotate |
key-revoke | 5 | /key/revoke |
Service-specific deviations:
consent.ma-atara.iometersGET/HEADwith a native limiter at 5000 / 60 s per IP —
crawlers are the intended traffic. Writes fall back to the shared tiers.
auth.ma-atara.iouses its own limiter at 20 / 60 s per IP for everything except/health.- On
keys.ma-atara.io,/healthand/api/capabilitiesare served before the limiter and are
not metered at all.
- Registration on
aegis.ma-atara.iocarries a second, per-DID limit on top of the per-IP tier:
60 / minute for POST /api/register and 20 / minute for POST /api/register/video.
Per-route limits inside api.ma-atara.io, applied in addition to the tier above:
| Route | Limit / 60 s per IP |
|---|---|
POST /api/headers/prepare | 60 |
POST /api/assets/mint, /api/assets/transfer, /api/assets/{id}/export | 30 each |
POST /api/inception/head | 30 |
POST /api/pqc/verify | 30 |
POST /api/anchors | 20 |
POST /api/governance/policy | 20 |
/api/pair/* (every method) | 20 |
POST /api/inception/org-officers | 10 |
POST /api/genesis/init | 5 |
POST /api/inbox/contact-request is the exception that does not use a 60-second window: 5 per 10 minutes per IP, and separately 30 per 24 hours per recipient DID, so a single owner cannot be flooded from many addresses.
Two caveats worth designing around. The shared limiter is backed by an eventually consistent store that tolerates roughly one write per second per key, so published limits are approximate under burst. When that store is degraded, the route uses a bounded in-isolate emergency bucket of 10 requests per IP per window, which is deliberately stricter than the tier table and never grants access or bypasses assertion checks. Handle 429 with Retry-After rather than assuming the tier number.
Shared-limiter 429s look like this:
{ "success": false, "error": "Rate limit exceeded", "retryAfter": 42, "tier": "read" }
Errors
Three response envelopes are in circulation. All are JSON.
Core worker structured errors — api.ma-atara.io, from validation and limiter paths:
{ "code": "invalid_request", "message": "Missing required fields" }
code is one of invalid_request, unauthorized, rate_limited, unsupported_alg, internal_error, not_found, forbidden, conflict. A details object and a retryAfter integer appear when relevant.
Core worker envelope — handler-level results and failures:
{ "success": false, "error": "Authentication required", "timestamp": 1785627968913 }
Do not match on the error string. The same 401 condition is reported as "Authentication required" on some routes and "unauthorized" on others. Branch on the status code.
Aegis envelope — every response on aegis.ma-atara.io, success or failure:
{
"success": false,
"error": "chunk.values must have 256 entries",
"timestamp": 1785627939971,
"requestId": "aegis-msb0rseb-1x6vroqr"
}
Quote requestId in any support conversation. did.ma-atara.io uses a fourth, smaller shape for its own errors: {"error": "cors_denied", "status": 403}.
| Status | Meaning in this API |
|---|---|
| 200 | Success. Note that POST /api/verify on aegis returns 200 only when the signature is valid |
| 204 | CORS preflight only |
| 301 | Compatibility redirect. Follow it once and update your client to the target |
| 400 | Malformed body, failed validation, replayed verification nonce, or an invalid signature on aegis POST /api/verify |
| 401 | Missing, malformed, expired, or replayed MaataraAssertion |
| 402 | Quota exhausted, or an org action requiring an active licence |
| 403 | Audience mismatch, ownership mismatch, or a browser Origin outside the allowlist |
| 404 | No such record. Consent lookups return 404 with {"found": false, …} by design |
| 405 | Method not allowed (docs.ma-atara.io accepts only GET, HEAD, OPTIONS) |
| 413 | Body or collection above the documented cap |
| 429 | Rate limited — read Retry-After |
| 503 | A dependency is unavailable, or an index is not provisioned on this deployment |
CORS
Server-side callers are unaffected by the origin allowlist: a request with no Origin header passes on every read surface, which is why the curl commands above work. Browser origins must be allow-listed per service.
One exception matters, because it fails in a way that looks like a bug: did.ma-atara.io requires an allow-listed Origin on mutations. POST /1.0/register sent without an Origin header returns:
{ "error": "cors_denied", "status": 403 }
Send an allow-listed Origin on POST, PUT, PATCH and DELETE to that host, including from servers. On consent.ma-atara.io, reads are open (Access-Control-Allow-Origin: *) while writes are origin-checked.
Endpoint reference
Auth column: none means no credential of any kind. assertion means a MaataraAssertion header. route-specific means the endpoint carries its own credential, described in the row.
api.ma-atara.io — core
Service metadata
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /, /index.html | none | Service banner |
| GET, HEAD | /health | none | {service, version, buildSha, timestamp} inside the success envelope |
| GET | /api/version | none | {version, commit, buildAt}. buildAt is generated at request time, not at build time |
| GET | /api/capabilities | none | Capability discovery. Served no-store and not wrapped in the success envelope |
| GET | /api/meta | none | Metadata and auth header hint |
| GET, HEAD | /openapi.json | none | OpenAPI 3 document generated from the live public route table |
| GET | /docs/API.md, /api/docs.md | none | Markdown rendering of the same spec |
| GET | /robots.txt | none | Allows /health and /openapi.json, disallows the rest |
GET /api/capabilities is the authoritative statement of what a deployment supports:
{
"version": "1.5.2",
"commit": "unknown",
"authSchemes": ["MaataraAssertion"],
"features": {
"headerPrepare": true,
"pqc": {
"mlKem": ["ml-kem-768"],
"mlDsa": ["ml-dsa-65"],
"legacyRecovery": "not accepted by native Veritas v4"
}
},
"storage": {
"default": "ipfs",
"gateway": "https://cloudflare-ipfs.com",
"pinning": false,
"provider": "r2",
"providers": ["r2", "external-http"]
},
"policy": {
"headerCanonicalization": "v2",
"aadHashAlg": "sha3-384",
"anchorHashAlg": "sha3-384",
"anchorHashAlgs": ["sha3-384"],
"dekWrap": {
"hkdf": {
"kdf": "HKDF-SHA384",
"salt": "aadHash",
"info": "maatara:dek-wrap:v1|ml-kem-768|aes-256-gcm|sha3-384",
"L": 32
},
"supported": ["aes-kwp", "aes-gcm"],
"recommended": "aes-kwp"
}
}
}
The capability response lists only the native ML-KEM-768 and ML-DSA-65 profile. Historical algorithm literals are not aliases, are not accepted by native Veritas v4, and must not be reintroduced as a compatibility fallback. storage.provider and storage.pinning are deployment-specific — read them rather than assuming them.
Chain
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/chain/append | assertion | Append a signed block to the caller's chain |
| GET | /api/chain/head | assertion | Head block index and hash |
| GET | /api/chain/blocks | assertion | List blocks in a range |
| GET | /api/chain/payload/{blockId} | assertion | Fetch one block payload |
| POST | /api/chain/rotation/activate | assertion | Activate a pending key rotation |
| GET | /api/chain/rotation/current | assertion | Current active rotation record |
| GET, POST | /api/security/hold | assertion | Read or place a chain-write freeze |
| POST | /api/security/hold/lift | assertion | Lift the freeze |
| GET | /api/sync/status | assertion | Device sync and reconciliation state |
Identity inception and genesis
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/genesis/init | route-specific | Create the genesis block for a new identity |
| GET | /api/genesis/{did} | none | Fetch a published genesis record |
| POST | /api/inception/head | assertion | Read or create the inception head. scope in the body must be the caller's own 96-hex chain scope, else 403 |
| POST | /api/inception/device, /device-event, /device-batch, /org | assertion | Hierarchical identity inception |
| POST | /api/inception/org-officers | route-specific | Rotate org officers. Requires an outgoing-officer quorum, incoming acceptances, and an org-key continuity signature |
Notes
Note bodies and attachments are encrypted on the device; these endpoints carry ciphertext.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/notes/{did} | assertion, DID owner | Append an encrypted note |
| GET | /api/notes/{did} | assertion, DID owner | List the notes index |
| GET | /api/notes/{did}/storage | assertion, DID owner | Bytes used |
| GET, DELETE | /api/notes/{did}/{noteId} | assertion, DID owner | Fetch or tombstone one note |
| GET | /api/notes/{did}/{noteId}/versions[/{v}] | assertion, DID owner | Version list, or one version |
| GET, PUT | /api/notes/{did}/{noteId}/attachments/{attId} | assertion, DID owner | Fetch or upload an encrypted blob |
| HEAD | /api/notes/{did}/cas/{cipherHash} | assertion, DID owner | Content-addressed dedup probe. Owner-gated on purpose: unauthenticated it was a cross-account correlation oracle |
| GET | /api/notes/{did}/cas/refs | assertion | Blob refcount index |
| POST | /api/notes/{did}/cas/gc, /api/notes/{did}/cas/rebuild | route-specific | Garbage-collect or rebuild refcounts. Authorised by a detached owner-key signature over a timestamp, accepted within ±5 minutes — not a bearer credential |
Inbox
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/inbox/contact-request | none | Begin a contact request for an assetId. Turnstile-gated; returns the owner's published public ML-KEM key for browser-side sealing |
| POST | /api/inbox/verify-otp | none | Verify the one-time code and deliver a browser-created encrypted envelope |
| GET | /api/inbox/{did} | assertion | List inbox |
| POST | /api/inbox/{did}/{msgId}/read | assertion | Mark read |
| DELETE | /api/inbox/{did}/{msgId} | assertion | Delete a message |
POST /api/inbox/contact-request requires only assetId, fromEmail, and turnstileToken. It returns {requestId, kemPublicB64u, kemKeyId}; the browser ML-KEM encapsulates to that public key and AES-256-GCM seals the name, email, subject, message, and asset context with request-bound AAD. POST /api/inbox/verify-otp accepts the OTP and that opaque envelope. Core rejects plaintext message fields and retains only the OTP-delivery email address in short-lived KV. The caller never supplies ownerDid. Core resolves the asset to its owner over the private Aegis service binding, then applies the per-recipient limit and resolves the owner's published ML-KEM key. A missing asset is 400 missing_asset; an unknown asset or unavailable internal resolution path is deliberately the same 404 owner_unresolvable, so the endpoint is not an asset-existence oracle. /reply is retired: reply through the owner's own mail client instead of posting plaintext mail content to Core.
Anchors
Anchors and the publisher model are covered in Anchors & callbacks.
POST /api/anchors records a root and creates the anchor record; writing that root to a public ledger is a separate step performed by an external publisher, which reports back over an internal callback that is not part of this contract. That step is not running on a schedule today — see the anchoring note under aegis.ma-atara.io — and the list is currently empty, so an anchor record here is a submission, not evidence of a chain write.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/anchors | none | List published anchors. ?limit= defaults to 100 and is capped at 500; ?cursor= pages. Returns {items, count, cursor} |
| GET | /api/anchors/{id} | none | One anchor record |
| GET | /api/anchors/state-proof | none | Rolling SHA3-384 Merkle certificate over the most recent anchor ids. Returns {window, count, root, leaves, createdAt, hashAlg, anchors} — every leaf is included, so you recompute the root yourself. ?window= defaults to 256, clamped to 1–2048. With no anchors published it is a root over an empty leaf set |
| POST | /api/anchors | assertion | Submit a root for anchoring |
| GET | /api/anchors/eligibility | assertion | Whether this owner should be offered an anchor |
Assets and governance
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/assets/mint, /api/assets/transfer, /api/assets/{id}/export | assertion | Mint, transfer, export |
| GET | /api/assets/{id}, /api/assets/{id}/receipts | assertion | Asset record, receipts |
| GET, POST | /api/governance/policy | assertion | Read the current policy document, or publish a version |
| GET | /api/governance/audit | assertion | Governance audit trail |
Device pairing
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/pair/offer | assertion | Create a pairing offer and one-time token |
| GET | /api/pair/offer/{code} | assertion | Retrieve your own offer |
| DELETE | /api/pair/offer/{code} | assertion, or the pairing token | Cancel an offer |
| POST | /api/pair/offer/{code}/join | route-specific: pairing token | Bind a joining device key |
| POST | /api/pair/seal | assertion | Seal a pairing exchange |
| GET | /api/pair/seal/{code} | route-specific: pairing token | Atomically consume the sealed result. Single-use |
Cross-device authorization relay (flag-gated)
The authorization relay delivers an approved device grant, not a root session token. Checked production source configuration enables device verification. When disabled in a destination deployment, these routes refuse with 403 device_keys_disabled. Source configuration is not a live deployment receipt. Use the approved Portal/device integration.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/authorize/offer | assertion, root only | Mint a cross-device authorization offer code and one-time token |
| GET | /api/authorize/offer/{code} | assertion, root only | Read the joined DeviceAuthorizationRequest with a fresh assertion |
| DELETE | /api/authorize/offer/{code} | assertion (root) or the offer token | Cancel an offer |
| POST | /api/authorize/offer/{code}/join | route-specific: offer token | Joining device binds its DeviceAuthorizationRequest |
| POST | /api/authorize/grant | assertion, root only | Root posts the DeviceGrant (attestation + wrapped MNK) for a joined offer |
| GET | /api/authorize/grant/{code} | route-specific: offer token | Joining device atomically consumes its DeviceGrant. Single-use — deletes on read |
The joining device uses its own authorized device session. Never substitute a root assertion or recovery phrase. See Authentication for the integration boundary.
Utilities
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/pqc/verify | none | Verify an ML-DSA-65 signature. See below |
| POST | /api/headers/prepare | assertion | Canonical header pre-image and aadB64u (SHA3-384). The session userId must equal body.userId |
| GET | /ipfs/{cid} | none | Read-through IPFS gateway proxy |
| POST | /api/ipfs/pin | assertion | Pin content |
| POST | /api/activation/redeem | route-specific | Redeem an activation code |
| POST | /api/user/verify | assertion-nonce | Verify the authenticated account against the licensing store |
| GET, POST | /invite-request | none | Invite request form and submission |
POST /api/pqc/verify verifies a signature over either a message or a SHA3-384 pre-hash:
curl -X POST https://api.ma-atara.io/api/pqc/verify \
-H 'content-type: application/json' \
-d '{"alg":"ml-dsa-65","signatureB64u":"…","publicKeyB64u":"…","messageB64u":"…"}'
# -> {"valid": true}
| Field | Required | Notes |
|---|---|---|
alg | yes | Send ml-dsa-65 for native evidence. An omitted or unknown value yields {"valid": false} |
signatureB64u | yes | base64url, unpadded |
publicKeyB64u | yes | base64url, unpadded |
messageB64u | one of | The signed message |
hashB64u | one of | A pre-hash of the message |
hashAlg | no | Only sha3-384 is accepted with hashB64u; anything else is 400 unsupported_alg |
The response is always {"valid": boolean} with status 200. A missing field is 400 invalid_request.
Internal — not part of the public contract
Reachable only over a Worker service binding, gated by an X-Maatara-Internal shared secret compared with timingSafeEqual (fails closed when the secret is unset). No third-party caller can reach these — they are documented here because aegis.ma-atara.io and did.ma-atara.io call them to verify a version-2 (device-signed) MaataraAssertion over their CORE_WORKER binding, per PORTAL_DEVICE_KEYS_V1 §4.5, and an integrator debugging a device_index_unavailable or attestation_unverifiable refusal from aegis or did-resolver needs to know the refusal is forwarded verbatim from one of these, not invented at the edge.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/internal/key-scope/{authKeyUserId} | Resolve a raw signing-key userId to its permanent rotation scope, or report it superseded (409) |
| GET | /api/internal/device/{deviceId} | Resolve a device key to its verified attestation record (owner DID, capabilities, revocation status) for aegis/did-resolver. ?consistency=primary bypasses both KV cache layers — capsule-registration callers MUST set it; session-minting and read routes MUST NOT |
| POST | /api/internal/nonce/consume | Atomically consume a dev:{deviceId} or attest:{scope} nonce on core's NONCE_GUARD Durable Object, for a caller (aegis) with no NONCE_GUARD binding of its own |
aegis.ma-atara.io — provenance
Metadata and verification
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health | none | Dependency roll-up. 503 unless every check passes |
| GET | /api/capabilities | none | Algorithm, limit and endpoint inventory |
| GET | /, /api/docs | none | Static HTML API documentation |
| POST | /api/verify | none | Verify one signature |
| POST | /api/verify/batch | none | Verify a batch |
| POST | /api/verify/with-key-status | none | Verify, and report the key's current lifecycle state |
The limits.rateLimit block inside /api/capabilities is advisory metadata that does not match what the limiter enforces. Use the rate-limit tables on this page.
POST /api/verify takes five fields, all required:
| Field | Notes |
|---|---|
publicKeyB64u | Signer's ML-DSA-65 public key |
messageB64u | The signed message |
signatureB64u | The signature |
nonce | Caller-chosen, single-use. Replay within the retention window returns 400 Duplicate nonce - potential replay attack |
algorithm | ml-dsa-65 for native evidence. Absent or anything else is 400 |
A valid signature returns 200 with data: {valid: true, keySource, nonceStatus, timingUs}. An invalid signature returns 400, not 200, with the same result object under error. Branch on the status code as well as on valid.
Registration and provenance records
Registration does not upload the original file. The worker never receives raw image bytes: capsules are produced on the device by @maatara/aegis-encoder and only the signed capsule is transmitted. The capsule's internal structure is proprietary and is not part of the public specification — treat capsule and provenance response bodies as opaque, and verify them through the endpoints below or with the encoder package.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/register | assertion, plus capsule quota | Register an image capsule |
| POST | /api/register/video | assertion, plus capsule quota | Register a video capsule |
| POST | /api/register/text | assertion, plus capsule quota | Register text fingerprint vectors. See below |
| POST | /api/commitment | assertion | Create a pre-registration commitment |
| GET | /api/commitment/{id} | none | Fetch a commitment |
| GET | /api/capsule/{assetId} | none | Fetch a capsule record |
| GET | /api/capsule/{assetId}/verify | none | Verify a capsule's signature |
| GET | /api/provenance/{assetId} | none | Returns 501 canonical_veritas_binding_unavailable until Aegis registration appends through the authoritative Core chain |
| GET | /api/provenance/{assetId}/path | none | Returns 501 canonical_veritas_binding_unavailable; Aegis service records are not relabelled as Veritas ancestry |
| GET | /api/provenance/{assetId}/proof | none | Evidence bundle — signature, public key and verification evidence, each labelled with the strength that actually backs it, plus a gaps list. Not a ledger inclusion proof: anchor is null with a stated reason. See below |
| GET | /api/anchor/status | none | Anchoring status. Reports what is deployed, not what is intended — see below |
| POST | /api/keypoints/upload | assertion | Upload a keypoint blob |
| GET | /keypoints/{96hex}.akpb | none | Fetch a keypoint blob |
The commitment hash is salted and does not expose the source bytes. The POST /api/commitment request has a closed contract containing only commitment_hash and the optional algorithm: "sha3-384"; free-text hints, hint commitments, client timestamps, and unknown fields are rejected. GET /api/commitment/{id} returns only the canonical commitment metadata and verification information. Historical records are projected into that same shape, so legacy plaintext labels are never retransmitted.
Quota exhaustion on the register routes returns 402:
{
"success": false,
"error": "quota_exhausted",
"message": "…",
"kind": "…",
"used": 0,
"limit": 0,
"tier": "…",
"requestId": "aegis-…"
}
Automatic ledger publication is disabled in checked configuration. Read GET /api/anchor/status and independently verify any claimed transaction. Ethereum is configured against a deployed registry on chain 11155111 — Sepolia testnet, not mainnet — and Bitcoin answers {"enabled": false, "status": "not-implemented"}. The scheduled anchoring trigger is disabled, so nothing is anchored on a timer; a single epoch was anchored by hand on 30 July 2026. That is why GET /api/provenance/{assetId}/proof returns anchor: null rather than omitting the field. A capsule's signature and the service-attested timestamp are real today; a public-ledger anchor is not, and a registration must not be presented as ledger-backed.
Similarity search
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/search/fragment | optional assertion — tier only | Fragment similarity search over registered image fingerprints |
| POST | /api/search/semantic | optional assertion — tier only | Semantic similarity search |
| POST | /api/search/video, /api/search/video/fragment | optional assertion — tier only | Video similarity search |
| POST | /api/query/text | optional assertion — tier only | Text similarity query. Fully specified below |
Fingerprints for the image and video routes are computed on the device by @maatara/aegis-encoder. Their request and response shapes are not published here; the text query contract below is.
Client assets
Static, publicly cacheable, and exempt from the origin allowlist check — a browser on an origin outside the allowlist gets the bytes instead of a 403. Exactly three path families are exempt: /models/, /ort/, and /keypoints/{96hex}.akpb (listed in the previous table).
That exemption is not the same as Access-Control-Allow-Origin: *. The worker rewrites CORS headers on every response on its way out, so a request carrying a non-allow-listed Origin receives no Access-Control-Allow-Origin header at all and a cross-origin fetch() from such an origin still fails at the browser. Server-side callers (no Origin) and allow-listed first-party origins are unaffected. Everything else on this host — including the on-device encoder's own WASM assets, which reach the first-party app through @maatara/aegis-encoder — is origin-checked and is not part of the public contract.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /models/manifest.json | none | Manifest of hash-pinned model artefacts used by the on-device encoder |
| GET, HEAD | /models/{file} | none | Fetch one artefact named in the manifest. Integrity is the caller's job: verify against the SHA3-384 pin in the manifest |
| GET | /ort/{version}/ort-wasm*.{wasm,mjs} | none | Self-hosted onnxruntime-web runtime mirror |
consent.maatara.io
consent.ma-atara.io remains an additional custom hostname during the domain cutover; the canonical product hostname is consent.maatara.io.
Discovery documents are cacheable and crawler-facing. The default policy for content with no record is opt-out.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health | none | Liveness |
| GET | /robots.txt | none | AI-crawler directives. Cache-Control: public, max-age=86400 |
| GET | /.well-known/ai-consent | none | Ma'atara discovery document with an AI-Pref draft-06 projection and the verification endpoint. max-age=3600 |
| GET | /.well-known/tdmrep.json | none | TDMRep Final Community Group Report site-rule array. max-age=3600 |
| GET | /api/v1/consent/{hash} | none | Single-hash lookup. max-age=300 on a hit, 60 s on a miss |
| GET | /api/v1/consent/by-creator/{did} | none | All records for a creator DID. max-age=120 |
| POST | /api/v1/consent/batch | none | Batch lookup, maximum 10,000 hashes per request |
| GET | /api/v1/spawning/opt-out | none | Spawning-compatible opt-out check. max-age=120 |
| POST | /api/v1/consent | route-specific: X-Aegis-Signature | Register a consent record |
| PUT | /api/v1/consent/{hash} | route-specific: X-Aegis-Signature | Update a record |
| DELETE | /api/v1/consent/{hash} | route-specific: X-Aegis-Signature | Revoke a record |
A hit returns the record plus an X-Aegis-Consent response header. A miss is 404 and still carries a usable body:
{ "found": false, "hash": "0000…", "message": "No consent record found. Default policy applies." }
Writes require the X-Aegis-Signature header — its absence is 401 — plus a canonical creator_did and acting public_key_b64u. Aegis verifies over a private service binding that the key speaks for that DID and verifies the ML-DSA-65 signature over shared canonical JSON. Updates and revocations require the existing native owner DID and the acting public key; ownerless historical rows return 409 non_native_consent_record rather than being adapted.
did.ma-atara.io
The resolution endpoint follows W3C DID Core and DID Resolution content negotiation. Resolution results use application/did-resolution; a successful raw DID document may be requested with Accept: application/did or application/did+ld+json. Responses use Cache-Control: no-store.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health | none | {status, service, version, commit, method: "did:maatara", timestamp} |
| GET | /.well-known/did.json | none | did:web self-description of the resolver |
| GET | /1.0/identifiers/{did} | none | Resolve a DID to a full resolution result |
| GET | /1.0/identifiers/{did}/bindings | assertion, member-scoped | The member's own org-binding roster |
| POST | /1.0/register | assertion with aud: "identity" | Owner-authenticated DID registration |
| GET | /1.0/org/{orgDid}/officers | none | Public officer set. Edge-cached 45 s fresh, 600 s stale-while-revalidate |
| GET | /1.0/org/{orgDid}/verify/{memberDid} | none | Is this member bound to this org — {orgDid, memberDid, bound, provider?, expiresAt?} |
| GET | /1.0/org/{orgDid}/members | org-controller or verified-officer assertion, active licence | Federated IdP-binding roster. 402 without a licence |
| GET | /1.0/org/{orgDid}/memberships?limit=50&cursor=... | org-controller or verified-officer assertion, active licence | Current folded membership roster: role, status, permission vocabulary, expiry/suspension window, and latest event provenance. Separate from IdP bindings; cursor-paginated (1–200). 402 without a licence |
| POST | /1.0/org/{orgDid}/bind, /revoke | org-owner assertion, active licence | Bind or revoke an IdP subject |
| GET, POST | /api/rotation/request | assertion, DID-controller authority | Request or poll a key rotation |
| POST | /api/rotation/activated | assertion, DID-controller authority | Mark a rotation activated. Bookkeeping only |
Status mapping, with the error carried in didResolutionMetadata.error:
| Status | didResolutionMetadata.error | Meaning |
|---|---|---|
| 200 | absent | Resolved; didDocument is populated |
| 400 | https://www.w3.org/ns/did#INVALID_DID | The identifier is not a well-formed did:maatara |
| 404 | https://www.w3.org/ns/did#NOT_FOUND | Well-formed, but no native document is published |
| 500 | https://www.w3.org/ns/did#INTERNAL_ERROR | The native authority projection is unavailable |
{
"@context": "https://w3id.org/did-resolution/v1",
"didDocument": null,
"didResolutionMetadata": {
"contentType": "application/did+ld+json",
"error": "notFound",
"retrieved": "2026-08-01T23:46:07.784Z"
},
"didDocumentMetadata": {}
}
keys.ma-atara.io
Authenticated routes on this host take a MaataraAssertion carrying aud: "keys"; an assertion with no aud, or a different one, is rejected. Legacy bearer/session credentials are not accepted on native routes. A missing Authorization header is 401 Authorization required. Browser origins must exactly match the worker's ALLOWED_ORIGINS list; an unset list never enables a development wildcard. Server-to-server requests without Origin remain supported, while public mutation helpers require an allowlisted Origin.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health | none | Status and binding inventory. Not rate limited |
| GET | /api/capabilities | none | Capability and policy list (retired: [...] names what used to be here). Not rate limited |
| PUT | /api/key-directory/{accountDid}/devices/{deviceId} | root assertion | Publish a signed DID-bound device credential and profile key package to the canonical KeyDirectoryDO |
| GET | /api/key-directory/{accountDid}/devices | assertion | List the account's public device packages through the strongly consistent directory |
| GET | /api/key-directory/{accountDid}/devices/{deviceId}?profile=...&suiteId=... | assertion | Resolve one device/profile package; no one-time prekey is returned by this read |
| POST | /api/key-directory/{accountDid}/devices/{deviceId}/prekeys/reserve | assertion + Idempotency-Key | Atomically reserve a compatible prekey under an explicit fallbackPolicy; bound v4 requires one-time-required |
| POST | /api/key-directory/{accountDid}/devices/{deviceId}/revoke | root assertion | Revoke the device package and supersede its prekeys |
| POST | /api/prekeys/upload, /api/prekeys/replenish | assertion | Sunset-bound migration inventory only; not accepted for native Veritas v4 or new channel bootstrap |
| GET | /api/prekeys/{userId} | assertion | Sunset-bound migration inventory only; not accepted for native Veritas v4 or new channel work |
| POST | /api/guardian/attest | assertion | Guardian threshold attestation |
| GET | /api/guardian/status/{id} | none | Guardian attestation status |
| GET | /api/epoch/current | none | Current public epoch. Served no-store |
| POST | /api/epoch/join, /api/epoch/ack | assertion | Epoch participation. user_id in the body must be the authenticated owner, else 403 |
| GET | /api/transparency/proof/{id}, /api/transparency/audit/{id} | none | Key-transparency proof and audit. Edge-cached for 5 s with no stale window |
Retired (PORTAL_DEVICE_KEYS_V1 §9.1 / ADR-0006), all now 410 endpoint_retired: the Key Status Registry (/api/key/status/, /api/key/history/, /api/key/rotate, /api/key/revoke, /api/key/report-compromise — never populated with anything a verifier consulted; core-worker's KeyLifecycleDO is the key-lifecycle authority); identity tiers (/api/identity/register, /api/identity/upgrade, /api/identity/{id}/tier — backed by a literal reputationScore field, which claim E retires); the legacy device-attestation surface (/api/identity/device/attest, /api/identity/devices, /api/identity/device/{id}, /api/identity/device/{id}/revoke — zero callers, superseded by device-attest.v1 / device-revoke.v2 on the account chain); the "Key Lifecycle" admin blade (/api/admin/keys, /api/admin/keys/{id}/revoke, /api/admin/keys/{id}/advisory — read/wrote the now-deleted UserKeyChainDO); the trust kernel and Dunbar layers (/api/trust/kernel/{id}, /api/trust/delegate, /api/trust/attestation, /api/trust/dunbar/{id}/{layer}, /api/trust/dunbar/assign — claim E: there is no device or key reputation system); and the two internal service-binding routes that only ever fed the deleted surfaces above (/api/internal/owner/register, /api/internal/device-key/resolve). UserKeyChainDO and the USER_KEYCHAIN binding are deleted; edge-key-fabric/src/{ksr,do/UserKeyChainDO.ts, identity/{device,device-key,tiers}.ts,internal/owner.ts,trust} no longer exist.
In the current source contract, the DID-bound KeyDirectoryDO is the canonical directory for new channel work because publication, revocation, and one-time-prekey reservation are serialized in one Durable Object. Its capability document currently reports keyDirectoryChannelReady: false: the implementation exists, but no secure-messaging production profile is active.
No epoch is active on production at the time of writing — GET /api/epoch/current returns {"epoch": null, "message": "No active epoch"}. Treat epoch participation as scaffolded rather than in service.
auth.ma-atara.io
Human sign-in only. This service does not issue credentials that protocol endpoints accept: core APIs still require a MaataraAssertion.
An OPAQUE login may authorize a client to unlock a locally wrapped, application-scoped secret. It must not be used to derive the DID signing root, secure-channel root, and recovery root from one export key; those roots remain independently generated and separately wrapped.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health | none | {service, version, commit, environment}. Not rate limited |
| POST | /api/auth/turnstile/verify | none | Cloudflare Turnstile verification and trust score |
| POST | /api/auth/opaque/register/init, /register/finish, /login/init, /login/finish | protocol-internal | OPAQUE aPAKE registration and login |
| POST | /api/auth/passkey/register, /api/auth/passkey/authenticate | protocol-internal | WebAuthn / FIDO2 |
| GET | /api/auth/google/start, /api/auth/google/callback | none | Google OAuth onboarding, redirect flow |
| GET | /api/auth/microsoft/start, /api/auth/microsoft/callback | none | Microsoft Entra / MSA onboarding |
| POST | /api/auth/onboard/ticket/verify | route-specific: onboarding ticket | Verify an onboarding ticket |
Every route except /health is limited to 20 requests per minute per IP. Unmatched paths return {"success": false, "error": "Not found"} with 404.
pay.ma-atara.io
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health | none | Liveness |
| GET | / | none | Service banner |
| GET | /pay/offer | none | Region-aware offers: {region, recommended, offers[], tiers, note} |
| GET | /pay/offer/{sku} | none | One SKU's price for the caller's region. 404 unknown_sku |
| POST | /pay/intent | assertion | Create a payment intent |
| GET | /entitlement | assertion | Current entitlement |
| POST | /entitlement/claim | none | Retired compatibility route. Always 410 entitlement_claim_retired |
| POST | /entitlement/redeem | assertion | Redeem an entitlement |
| GET | /quota | assertion | Quota meters |
| POST | /quota/delta | assertion | Adjust a quota meter |
| POST | /quota/consume | assertion | Atomic check-and-increment. 402 when the increment would breach the tier cap |
| POST | /quota/refund | assertion | Return a previously consumed unit. Floored at zero, so a double refund cannot manufacture allowance |
Region is resolved from the caller's IP and can be overridden with ?country=. Tiers are notes, provenance, atelier and research; Notes is free on signup and Research is contact-sales.
curl https://pay.ma-atara.io/pay/offer/provenance_yearly
{
"sku": "provenance_yearly",
"display": "Provenance (yearly)",
"kind": "subscription",
"tier": "provenance",
"interval": "year",
"price": { "amount_minor": 12000, "currency": "AUD", "display": "A$120/yr" },
"region": "AU"
}
CORS applies to /pay/, /entitlement and /quota*.
docs.ma-atara.io
Documentation is open in production — no token is required. Only GET, HEAD and OPTIONS are accepted; anything else returns 405 with Allow: GET, HEAD, OPTIONS.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health | none | Liveness, no-store |
| GET | / | none | Documentation home |
| GET | /developer, /developer/{slug} | none | Developer documentation |
| GET | /academic, /academic/{slug} | none | Academic documentation |
| GET | /sitemap.xml, /robots.txt | none | Sitemap and crawler directives |
Every response carries HSTS (2 years, includeSubDomains; preload), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, and a CSP of default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'.
Text similarity: the current contract
Both text endpoints on aegis.ma-atara.io accept vectors only. They no longer accept prose in any form.
Plaintext is rejected
If the request body contains any of text, title, body or content, the request fails with 400 and this exact message — the offending field name is interpolated:
{
"success": false,
"error": "this endpoint no longer accepts 'text'. Fingerprint on the device with @maatara/aegis-pure and send vectors.",
"timestamp": 1785627939788,
"requestId": "aegis-msb0rs98-jrtgirxh"
}
The check is for presence of the key, not its value: "text": null and "text": "" are rejected exactly like "text": "hello world". Fields are checked in the order text, title, body, content, and the first one found is named in the message. On POST /api/register/text this check runs after authentication, so an unauthenticated caller sees 401 first.
Fingerprint on the device
Vectors are produced locally by @maatara/aegis-pure, which is dependency-free and runs identically in a browser, a worker and Node.
npm install @maatara/aegis-pure @maatara/pqc-toolkit
import { fingerprintText, fingerprintQuery } from '@maatara/aegis-pure';
import { createProtocolWasm } from '@maatara/pqc-toolkit';
// Browser/Vite asset import; see Packages for other runtimes.
import wasmUrl from '@maatara/pqc-toolkit/wasm/binary?url';
const proto = createProtocolWasm();
const glue = await import('@maatara/pqc-toolkit/wasm/glue');
await proto.init(wasmUrl, { glue });
const sha3String = (s: string) => proto.hash.sha3String(s); // SHA3-384 hex of a UTF-8 string
// Registration side — returns the asset id, counts and the ±1 vectors.
const fp = fingerprintText(sha3String, documentText);
// Query side — same pipeline, capped so a long query cannot fan out.
const q = fingerprintQuery(sha3String, queryText);
The one input you supply is sha3String. Both sides use the same WASM implementation, so the digest is identical by construction rather than by agreement.
The package returns camelCase; the wire is snake_case. Neither function returns a request body, and posting one verbatim fails validation. Map it yourself:
// POST /api/register/text
{ asset_id: fp.assetId, word_count: fp.wordCount, chunks: fp.chunks }
// POST /api/query/text
{ vectors: q.values, word_count: q.wordCount, chunk_count: q.chunkCount }
fp.chunks already has the field names the endpoint wants (index, values, start, words), so only the two top-level keys are renamed. On the query side the array is called values in the package and vectors on the wire — send q.values under the name vectors, or you get 400 vectors must be a non-empty array. Use the released query defaults and honor the endpoint limits.
What this means for trust. Because the server can no longer re-derive a fingerprint from the text, it cannot check that a submitted vector corresponds to any particular prose. Abuse is constrained by the caller's quota and their authenticated DID, not by content verification. There is no server-side attestation that a registered fingerprint matches any specific document.
POST /api/register/text
Requires a MaataraAssertion and consumes capsule quota. Returns 503 text index not provisioned if the deployment has no text index bound.
{
"asset_id": "text-0123456789abcdef0123456789abcdef",
"word_count": 1234,
"chunks": [
{ "index": 0, "values": [1, -1, "… exactly 256 entries, each exactly 1 or -1 …"], "start": 0, "words": 200 }
]
}
| Check | Message | Status |
|---|---|---|
| JSON parses | invalid-json | 400 |
| No plaintext fields | see above | 400 |
asset_id matches ^text-[0-9a-f]{32}$ | asset_id must match text-<32 lowercase hex> | 400 |
chunks is a non-empty array | chunks must be a non-empty array | 400 |
| At most 5000 chunks | too many chunks | 413 |
word_count is an integer ≥ 5 | word_count must be an integer >= 5 | 400 |
| Each chunk is an object | chunk must be an object | 400 |
values is an array | chunk.values must be an array | 400 |
values has 256 entries | chunk.values must have 256 entries | 400 |
| Each value is exactly 1 or -1 | chunk.values entries must be exactly 1 or -1 | 400 |
index is a non-negative integer | chunk.index must be a non-negative integer | 400 |
Success is 200 with a raw body — no {success, data} envelope — and an X-Request-ID header:
{ "asset_id": "text-…", "chunk_count": 12, "word_count": 2400, "duplicate": false, "registered_at_ms": 1754006400000 }
Registration is idempotent: re-registering the same asset_id returns 200 with "duplicate": true. The stored manifest carries counts and the asset id only — no prose, no excerpt, no title.
POST /api/query/text
Unauthenticated by design. An index nobody can query is useless. Sending a verifying assertion with aud: "provenance" raises the rate limit from 10 to 60 per minute and changes the metering key from your IP to your DID; it does not change the result set.
curl -X POST https://aegis.ma-atara.io/api/query/text \
-H 'content-type: application/json' \
-d '{"vectors":[[1,-1, … 256 entries …]],"limit":20,"minScore":0.6}'
| Field | Required | Notes |
|---|---|---|
vectors | yes | Non-empty array of vectors, at most 16. Each is exactly 256 entries of 1 or -1 |
limit | no | Defaults to 20, clamped to 1–100 |
minScore | no | Defaults to 0.6, clamped to 0–1. Matches below it are dropped |
word_count | no | Echoed back; never used in matching |
chunk_count | no | Echoed back; never used in matching |
Validation failures, all 400: invalid-json; the plaintext rejection above; vectors must be a non-empty array; at most 16 query vectors; and the per-vector messages chunk.values must have 256 entries and chunk.values entries must be exactly 1 or -1.
Success is 200, raw JSON, best score per asset, sorted descending:
{
"matches": [{ "asset_id": "text-…", "owner_ref": "0123456789abcdef0123456789abcdef", "chunk_idx": 3, "score": 0.87 }],
"query_word_count": 500,
"query_chunk_count": 3,
"query_chunks_used": 3
}
owner_ref is not the DID. It is a stable, opaque reference derived from the owner's DID under a domain-separated SHA3-384. An anonymous caller can learn that two matches share an owner, and cannot invert the value to recover a DID. Do not read it as anonymity: it is a pseudonym — a deterministic function of an identifier that is public elsewhere in this system (ownerDid is a required field of every Veritas v4 block, and GET /api/capsule/{assetId} is unauthenticated). Anyone holding a _candidate_ DID can compute that DID's owner_ref and confirm a match. The reference reduces direct identifier disclosure; it is not a guarantee against attribution. This endpoint previously returned the raw DID — if you have code that reads did from a match, it is reading a field that no longer exists.
Packages
Four package manifests are publishable in the source tree and all four have npm releases. The two core packages have paired 2.2.1 workspace and registry versions, checked on 21 September 2026. Version equality alone does not establish artifact identity or service deployment.
| Package | Source-tree version | npm registry | Use |
|---|---|---|---|
@maatara/pqc-toolkit | 2.2.1 | 2.2.1 | Low-level crypto plus experimental pairwise- and hybrid-channel entry points |
@maatara/veritas-chain | 2.2.1 | 2.2.1 | Provenance-only chain model, canonical validation, receipts, and Merkle anchor tree; no secure-channel API |
@maatara/aegis-pure | 0.2.0 | 0.2.0 | Dependency-free provenance helpers, key-status validity, on-device text fingerprinting |
@maatara/aegis-encoder | 0.1.0 | 0.1.0 | On-device provenance capsule encoding, browser-side |
This page describes reviewed integration contracts; service builds and package releases are separate. Confirm the registry versions before pinning an external integration. Secure-messaging orchestration remains private in @maatara/secure-channel, with no production profile active.
Not documented here
Internal service-binding endpoints, administrative surfaces, debug routes and deprecated compatibility redirects are deliberately absent. They are not part of the public contract, are not supported for third-party use, and may change or disappear without notice. If you find one, do not build on it. The three /api/internal/* device-keys routes under api.ma-atara.io are the one deliberate exception, listed above because their refusals surface through aegis and did-resolver responses an integrator will actually see — that entry does not make them callable from outside a service binding.