From an API key to a sealed, witness-cosigned, offline-verifiable verdict on a financial claim, in five steps. Every snippet on this page runs verbatim against the production API and was validated against it before publication.
You need an API key. Keys are issued by the Artizar founders under the design-partner program; the raw key is shown once at issuance and never retrievable again. Every request carries it:
Authorization: Bearer artz_<key-id-hex>_<secret>
Store it in your secret manager and export it as ARTIZAR_API_KEY
for the snippets below. Any authentication failure (missing, malformed, revoked) returns
the same generic 401. Rotate the key on receipt (step 4): the copy the operator
saw at issuance is then dead.
One financial-statement claim: "metric for period was value unit", about a filer identified by ticker or CIK.
curl -s https://api.artizar.ai/v1/verifications \
-H "Authorization: Bearer $ARTIZAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ticker": "AAPL",
"metric": "revenue",
"period": "FY2023",
"claimed_value": "383285000000",
"unit": "USD"
}'
ticker (resolved against a
content-hash-pinned SEC snapshot; unknown or ambiguous tickers are refused, never
guessed) or cik (the precision input; pass it directly if you have it).metric ∈ revenue | net_income | eps |
gross_profit | operating_income | operating_cash_flow | rnd_expense |
total_assets | stockholders_equity | capex | ebitda_calculated |
interest_expense | income_tax_expense | depreciation_amortization |
free_cash_flow_calculated | total_debt | shares_outstanding | cash. stockholders_equity is
parent-only (excludes noncontrolling interests); every equity verdict states the
definition in a context note. ebitda_calculated is the published
GAAP-component formula (net income + income taxes + interest expense + D&A,
summed exactly), not the filer's non-GAAP Adjusted EBITDA; every such
verdict states the formula and that distinction in context notes, and a filer
missing any component is no_evidence, never an approximation.
interest_expense, income_tax_expense and
depreciation_amortization use exactly the definitions of those EBITDA
components, so the calculated figure reconciles against them term by term; each
verdict states its definition in a context note, and a net tax benefit is a negative
income_tax_expense.
free_cash_flow_calculated is the published formula operating cash flow
minus capital expenditures (exactly the operating_cash_flow and
capex definitions, so it reconciles against them term by term). It is
not the company's own reported free cash flow, which has no uniform
definition; every such verdict states the formula and that distinction in context
notes, and a filer missing either component (banks, or filers that tag capital
expenditures under a broader concept) is no_evidence, never an
approximation. A negative figure is real and verifies as a negative number.
total_debt refuses as unmapped_metric: no consistently
taggable debt definition exists across filers, and
Artizar refuses rather than approximates. Metrics a filer does not report
(a bank's gross_profit) land no_evidence, honestly, with a
no_fact_under_definition note naming the concepts the definition uses.
Any other name is a 422 unknown_metric
(not charged), whose allowed_values lists every supported name. Artizar
counts the normalized name, per tenant and day, to prioritize new metrics.period: annual (FY2023 /
Y2023) and quarterly (Q1FY2024 / Q1Y2024).
FY means the company's own fiscal-year label, resolved from its
actual filings: a non-calendar filer's FY2023 is what that company calls
fiscal 2023, not a calendar approximation. If the filings make a label genuinely
ambiguous (a fiscal-year-end transition), the claim is refused rather than guessed.claimed_value is an exact decimal string
(floats are rejected); unit is ISO-4217 style (USD),
USD/shares for eps, shares for
shares_outstanding.market (optional) defaults to "edgar":
US SEC filings, the v1 coverage. "tadawul" (Saudi Exchange) is accepted by
the schema and currently returns a typed unsupported_market refusal while
the retrieval layer is built, with no silent fallback to the wrong market.422). Nothing you send can influence the
verdict: the tier is computed by Artizar's deterministic scorer from primary-source
evidence alone.The 201 response is a sealed verdict: the deterministic
outcome, the evidence chain to the primary source as filed, the version stamps that make
it reproducible, and an Ed25519 seal binding all of it to a leaf of Artizar's public
transparency log.
{
"api_version": "v1",
"sealed": {
"verdict": {
"outcome": "confirmed", "tier": "green",
"subject_market": "edgar", "subject_issuer_id": "0000320193",
"subject_cik": "0000320193",
"metric": "revenue", "period": "FY2023",
"claimed_value": "383285000000", "claimed_unit": "USD",
"verified_value": "383285000000", "verified_unit": "USD",
"refusal_reason": null,
"rationale": "…cites the EDGAR URL + content hash…",
"evidence": [ { "document_url_stable": "https://data.sec.gov/…", "content_hash": "…", "…": "…" } ],
"context_notes": [
{ "code": "fiscal_frame", "text": "FY2023 is the filer's own fiscal label: the bound fact's period ends 2023-09-30, …", "origin": "deterministic" },
{ "code": "revenue_tag", "text": "Confirmed under us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax — …", "origin": "deterministic" }
],
"notes_version": "notes-v6",
"verdict_schema_version": "verdict-v4", "binding_version": "binding-v4",
"scoring_policy_version": "policy-v7", "lexicon_version": "lexicon-v7"
},
"record_id": "5b332ef9-…", "tenant_id": "your-tenant",
"commitment": "…64-hex…", "commitment_version": "commit-v1",
"leaf_index": 757, "entry_hash": "…64-hex…",
"sealed_at": "…", "seal_version": "seal/v1",
"seal_key_id": "2d7421f9…", "seal_signature": "…hex…"
}
}
A verdict is never a binary "true." confirmed means
the primary source (SEC EDGAR XBRL) states the claimed value within tolerance,
with the filing URL, accession number, and content hash attached so you can check the
source yourself. The five version stamps make a re-verification under the same stamps the
same computation: reproducible, not merely asserted.
Where the record warrants it, the verdict carries context_notes: up to 8
structured {code, text, origin} caveats an agent can branch on or put
straight into a spreadsheet cell, e.g. fiscal_frame (the period label is
the filer's own fiscal frame, not a calendar period),
instant_asof (the balance-sheet date a point-in-time value is as-of), or
metric_definition_equity (stockholders' equity is parent-only; the
including-NCI figure is a different fact). Every ebitda_calculated verdict
carries derived_definition (the published formula) and
calculated_vs_adjusted (this is the GAAP-component figure, not the
company's non-GAAP Adjusted EBITDA, which can differ materially). Every
free_cash_flow_calculated verdict carries the same two codes: its formula,
with the minus sign, and the caveat that a company's own reported free cash flow may
be defined differently.
Every note is deterministic:
derived by a closed, versioned rule table (notes_version), never authored
by a model, and notes never influence the tier. The full vocabulary lives in the trust
pack's verdict schema. Refusal verdicts get actionable notes too: an
unmapped_metric refusal names the currently supported metric set.
When Artizar cannot verify, it says so in a sealed verdict of its own:
outcome="not_verifiable", tier="no_rating", and a
machine-readable refusal_reason you can branch on:
unresolved_subject (retry with a CIK), unmapped_metric,
unsupported_value, unsupported_market,
ambiguous_fiscal_label (the filer's own annual filings do not assign the
requested fiscal period one label; refused, never guessed). Refusals are sealed,
committed, retrievable, and charged like any verdict: they are provable output.
Safe retries. Send an Idempotency-Key header
(a unique value per request, up to 255 visible ASCII characters; a UUID is ideal). A retry
with the same key and the same body within 24 hours returns the original sealed verdict as a
200 with Idempotent-Replayed: true: it is not charged again and adds
nothing to the public log. The same key with a different body is a
409 idempotency_key_reused. Without the header, submissions are deliberately
not idempotent: resubmitting the same claim is a fresh verification, a fresh
charge, and an unlinkably fresh public commitment. That unlinkability is a privacy feature.
Nobody reading the public log can see what you verified or how often.
Your record is retrievable immediately; its witnessed proof exists after the next cosigning of the public tree.
# The full committed record (verdict, evidence, blinding factor, commitment, log anchor): curl -s https://api.artizar.ai/v1/verifications/<record_id> \ -H "Authorization: Bearer $ARTIZAR_API_KEY" # The witnessed inclusion proof (checkpoint + entry + Merkle path): curl -s https://api.artizar.ai/v1/verifications/<record_id>/proof \ -H "Authorization: Bearer $ARTIZAR_API_KEY" # The stored sealed verdict (recover your POST-response artifact byte-identically): curl -s https://api.artizar.ai/v1/verifications/<record_id>/sealed \ -H "Authorization: Bearer $ARTIZAR_API_KEY"
Lost the 201 body? GET …/sealed serves the
stored sealed verdict: the exact signed bytes, never re-derived, so a seal-key rotation
can never vanish your history (verify it against the published key set for its
seal_key_id). A record whose seal predates durable storage answers
404 seal_not_available honestly.
The proof route serves only witness-cosigned tree heads: until the next
cosigning covers your leaf it returns 404 proof_not_available with a
Retry-After header; wait that long, and the first 200 carries a
witnessed checkpoint. The POST already told you the window:
proof_expected_after_seconds, counted from sealed.sealed_at. Run the three
checks below before archiving a proof regardless: they are your independence from
Artizar, not a workaround. The sealed verdict deliberately bridges the wait: it already
binds your verdict to the exact leaf (leaf_index/entry_hash)
the proof will cover, so you can act on the verdict at once and archive the proof when
it lands.
Artizar's public keys are published at
artizar.ai/.well-known/artizar-keys.json
(the seal key, the log key, and the witness keys). With those and the responses above,
your compliance code re-verifies everything with no call back to Artizar.
The reference verifiers ship in the artizar client package included in your
onboarding bundle; the checks are also fully specified for independent reimplementation:
standard Ed25519, SHA-256, and RFC-6962-style Merkle verification, no Artizar SDK
required:
from artizar.audit import AuditLogEntry, CosignedTreeHead
from artizar.audit.merkle import InclusionProof
from artizar.audit.monitor import verify_record_against_checkpoint
from artizar.seal import SealedVerdictRecord, verify_sealed_verdict
from artizar.types import CommittedVerificationRecord, compute_commitment
# 1. The seal: Artizar signed THIS verdict, bound to THIS record + commitment.
sealed = SealedVerdictRecord.model_validate(post_response["sealed"])
assert verify_sealed_verdict(sealed, seal_public_key_hex=ARTIZAR_SEAL_KEY).valid
# 2. The record: the public commitment is exactly the hash of what you hold.
record = CommittedVerificationRecord.model_validate(record_response["record"])
assert compute_commitment(record.record) == record.commitment == sealed.commitment
# 3. The tree: known witnesses attest your leaf is in the public append-only log.
entry = AuditLogEntry.model_validate(proof_response["entry"])
assert verify_record_against_checkpoint(
entry.entry_hash,
InclusionProof.model_validate(proof_response["inclusion_proof"]),
CosignedTreeHead.model_validate(proof_response["checkpoint"]),
log_public_key_hex=ARTIZAR_LOG_KEY,
witness_public_keys=ARTIZAR_WITNESS_KEYS,
)
assert entry.entry_hash == sealed.entry_hash # the seal's anchor IS the proven leaf
Hand the record + proof to your own auditor or regulator: whoever holds them can recompute the commitment and check the public tree without an Artizar account.
Roll your credential yourself, with zero downtime, including the very first key you were handed:
# 1. Mint a successor (your current key stays live; deploy at your own pace):
curl -s -X POST https://api.artizar.ai/v1/keys/rotations \
-H "Authorization: Bearer $ARTIZAR_API_KEY"
# → 201 { "key_id": "…", "raw_key": "artz_…" } # shown once: store it now
# 2. When the new key is deployed, retire the old one WITH the new key:
curl -s -X POST https://api.artizar.ai/v1/keys/<old-key-id>/revocations \
-H "Authorization: Bearer $ARTIZAR_NEW_KEY"
# → 201; the old key's next request is a 401
At most two keys may be active at once; a rotation while a previous
one is unfinished is 409 rotation_blocked (revoke the retired key first).
You can never revoke your last active key
(409 last_active_key): rotate first, so you always hold a working
credential. Every rotation is on the append-only record: the new key's issue event names
the key that minted it. Rotation never resets your quota or rate limits; both follow
your tenant, not the key.
Give any MCP-compatible agent the API as tools. There are two ways in: the hosted endpoint, for agent platforms that cannot run a local process, and a local server on your own machine. Either way, authentication, quota, rate limits, and above all the deterministic tier are enforced server-side, and every result is the API's own JSON.
https://api.artizar.ai/mcpPoint your agent's MCP client at the endpoint over Streamable HTTP and send your key as
a bearer token, Authorization: Bearer artz_…, on every request. The endpoint
is stateless (there is no session to keep), answers in JSON, and serves both protocol
eras: the stateless 2026-07-28 revision and the handshake revisions back to
2024-11-05. Without a valid key, every request is a 401, before
any tool is listed.
curl -s https://api.artizar.ai/mcp \
-H "Authorization: Bearer $ARTIZAR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-11-25" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Four tools: verify_financial_claim (the Step 1 request body as arguments,
plus an optional idempotency_key), get_verification,
get_verification_proof, and list_supported_metrics (free: the
node's own metric registry, with each metric's definition, period kind, and unit, stamped
with its lexicon version). A result is exactly the JSON the matching REST route
returns.
Retries and refusals. Give each claim an
idempotency_key and reuse it when you retry: the retry returns the original
sealed verdict without a second charge (the rule of the Idempotency-Key
header, and the two share one namespace, so the same key and claim through either door is
one verification). A refusal is a tool error whose structured content is the
error body (code, retryable,
retry_after_seconds, doc_url), so an agent can act on it
without parsing prose. Results are quoted filing data, never instructions.
Give an agent host on your machine (Claude Code, Claude Desktop, Cursor, …) three tools over this API: verify a claim, fetch the record, fetch the proof. The server is a strict HTTP client running on your machine; it adds no policy, and the sealed verdict passes through byte-faithful.
# from the client wheel in your onboarding bundle: pip install 'artizar[mcp] @ file:./artizar-<version>-py3-none-any.whl'
{
"mcpServers": {
"artizar-verify": {
"command": "artizar-mcp",
"env": {
"ARTIZAR_API_URL": "https://api.artizar.ai",
"ARTIZAR_API_KEY": "artz_…"
}
}
}
}
Both variables are required; the server refuses to start without them, and the error names the variable and never echoes a value.
curl -s https://api.artizar.ai/v1/usage \
-H "Authorization: Bearer $ARTIZAR_API_KEY"
# → { "tenant_id": "…", "period": "2026-07", "limit": 1000, "used": 41, "remaining": 959, … }
Your own tenant only: the endpoint takes no tenant parameter.
limit/remaining are null when no quota grant is
active. Billing counts one metered charge per produced verdict (refusals
included, since they consumed the same verification work and are equally provable); rejected
requests (4xx) and outages are never charged. Usage is per calendar month, UTC.
Every error is one JSON envelope: {"error": {"code", "message", "details", ...}}.
code is stable; details lists the failing input locations and never
the values you sent. The other fields appear only when they apply:
retryable: whether sending the identical request again can succeed. Every
error carries it.retry_after_seconds: how long to wait, when Artizar knows. It always equals
the Retry-After header.param: the one failing input (for example body.metric), when
exactly one failed.allowed_values: the accepted values for metric or
market, when that is what failed.doc_url: a link to this code's row below.| Status | error.code | Meaning | Retry? | What to do |
|---|---|---|---|---|
| 400 | error | the request body could not be read | no | send a complete, well-formed request |
| 401 | unauthenticated | missing/invalid/revoked key (one generic body) | no | check the key; rotate if needed |
| 403 | forbidden | the key lacks this capability (not reachable with a normal key) | no | contact Artizar |
| 404 | not_found | unknown record id, or not yours | no | check the id |
| 404 | proof_not_available | your record is newer than the latest witnessed head | yes, after retry_after_seconds | wait, then fetch the proof again; the POST's proof_expected_after_seconds gives the same window up front |
| 404 | no_checkpoint | no witnessed tree head exists yet | yes, after retry_after_seconds | wait, then retry |
| 404 | seal_not_available | the record predates durable seal storage | no | use the record and its proof |
| 405 | method_not_allowed | wrong HTTP method for this path | no | use a method listed in the Allow header |
| 409 | idempotency_key_reused | this Idempotency-Key was already used with a different request | no | use a new key for a new request |
| 409 | rotation_blocked | two keys are already active | no | revoke one, then rotate |
| 409 | last_active_key | revoking this key would leave the tenant with none | no | rotate first, then revoke |
| 409 | key_already_revoked | the key is already revoked | no | nothing to do |
| 413 | payload_too_large | the request body is larger than 1 MiB | no | send a smaller body |
| 422 | invalid_request | malformed body / unknown field (locations listed, values never echoed) | no | fix the request; param and allowed_values say what to change |
| 422 | unknown_metric | metric is not a supported name (the value is never echoed; not charged) | no | use a name from allowed_values (exact, lowercase) |
| 429 | rate_limited | per-IP or per-tenant rate limit: back off | yes, after Retry-After | honor Retry-After; the RateLimit-* headers help you self-throttle |
| 429 | quota_exhausted | monthly quota spent (no Retry-After): buy more | no | contact Artizar; resets at the calendar month (UTC) |
| 500 | internal_error | an unexpected server error; quote the trace_id in the message | a GET: yes. The POST: only with its original Idempotency-Key | retry a GET; resend a POST only with the same key, which is charged at most once (without a key, a retry may be charged twice) |
| 503 | verification_unavailable | SEC EDGAR unreachable; you were not charged | yes, with backoff | retry with exponential backoff; a retry cannot double-charge |
Rate limits are layered: per-IP (availability) and per-tenant (follows
your credential; key rotation or multiple egress IPs don't change it). Batch clients
should honor Retry-After and pace themselves. Artizar's own benchmark
client does exactly that against this same API.