# API and MCP operations: errors, limits, retries and long tasks [API quickstart](/api/quickstart.txt) · [MCP guide](/api/mcp) · [Generated OpenAPI](/api/openapi.json) · [MCP catalogue](/api/mcp/tools.json) · [Interactive Swagger](/docs) · [ReDoc reference](/redoc) · [Local sandbox](/api/sandbox) ## Compatibility and deprecation `/v1` is the REST contract major version. OpenAPI `info.version` identifies the pipeline release, not a replacement URL version. `x-resolvd-revision` records runtime/schema fingerprints so evaluations can compare actual implementations. MCP negotiates its protocol version separately and publishes current tool schemas. Within `/v1`, retain existing endpoint paths, required inputs, established field types and meanings. New optional inputs must preserve behavior when omitted; responses may gain fields, so clients should tolerate unknown response fields. Breaking removals or incompatible schema changes require a new major contract and migration instructions. Provider facts, calibrated policy and resulting decisions can evolve with dated data/pipeline revisions; a compatible schema does not promise identical matching results forever. Deprecation policy: mark an affected operation `deprecated: true` in OpenAPI, publish its replacement, migration steps and proposed retirement date in the release notes before removal, and keep it operational during the announced migration interval. Do not silently remove a `/v1` operation. Security fixes may tighten authorization to close an exposure; their migration notes must identify the changed permission and required owner action. Current deprecated operations: none. No retirement date is announced unless the operation's deprecation notice explicitly supplies one. ## Authentication and machine-readable contracts Send `Authorization: Bearer ` over HTTPS. Do not send keys in URLs. Request only the scopes accepted by the operation; OpenAPI's `x-accepted-key-scopes` records alternatives, not cumulative requirements. The MCP catalogue carries each tool's own schema and metadata. Reference and receipt reads require authorization even though they consume no credits. ## Error envelopes Non-OAuth REST errors use `error.code`, `error.message`, `error.correlation_id` and `error.details`. OAuth follows its own OAuth error contract. The following is a **format example**, not a captured live failure: ```json { "error": { "code": "rate_limited", "message": "Too many requests", "correlation_id": "req_example", "details": {"retry_after": "5"} } } ``` MCP application errors set `isError: true` and carry the error envelope in `structuredContent`; SDK schema rejection can instead carry validation content without application metadata. Authentication can fail at the HTTP boundary with an OAuth-style `error` string and bearer challenge. Preserve the correlation ID when reporting a problem. Internal/storage errors are deliberately opaque. Field validation errors require corrected input. | Response or condition | Client action | |---|---| | `401` / `unauthorized` | Obtain a valid credential; follow the OAuth challenge when applicable. | | `403` / `key_scope_forbidden` | Ask the owner for a scope accepted by this operation. | | `409` / idempotency conflict | Bind each operation key to one unchanged request. | | `413` / body too large | Reduce payload size using the published limits. | | `422` / invalid input | Fix fields against the schema; unknown arguments are rejected. | | `429` / `rate_limited` | Wait `Retry-After`; use bounded backoff, jitter and a retry limit. | | `503`, deadline or transport interruption | Keep the operation key; inspect and reconcile state. | | `unresolved` | Completed abstention: inspect clarification; do not loop unchanged input. | | Evaluation refusal | Inspect its code and balances; never silently lower depth. | ## Pace calls using returned limits REST publishes `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` on rate-limited operations; refusals publish `Retry-After`. Reset and retry values are seconds, not Unix timestamps. Use the headers on your response: limits depend on the operation, credential plan and current shared bucket. Read authenticated `/v1/reference/limits` for configured limits and `/v1/usage` for usage. MCP tool results publish `_meta.resolvd.correlation_id` and `_meta.resolvd.rate_limit` with `scope`, `limit`, `remaining`, `reset_seconds` and `retry_after_seconds`. Inspect this metadata as well as the error envelope and HTTP response. Zero retry seconds means the rate gate admitted the call, not that provider work or billing succeeded. A rate bucket is not a credit balance. Configured base token-bucket capacities before plan multipliers: | Operation bucket | Capacity / maximum full-bucket burst | Continuous refill / second | |---|---:|---:| | Read | 1000 | 16.6667 | | Resolve | 300 | 5 | | Match | 300 | 5 | | Batch | 30 | 0.5 | The credential's plan multiplier scales capacity and refill; returned headers publish effective values. A full bucket permits up to its capacity immediately, then refills continuously. These are admission ceilings, not a throughput SLA. Batch calls have separate row/compute admission and credit constraints. REST/MCP share tenant buckets with the same Redis state; without Redis, buckets are local to each process. Per-IP gates and provider/concurrency limits can also refuse a call. No advertised burst bypasses those limits. The configured request deadline is 300 seconds (zero disables it). The batch ceiling is 500 records. Client timeouts should allow the server's deadline and response transfer. Lower concurrency when a concurrency gate refuses work; aggressive retries make the same queue worse. Do not split a request merely to evade admission. ## Idempotency, replay and charging For REST computation use `Idempotency-Key`; MCP computation tools accept `idempotency_key`. Send the same key and same payload on a retry. A changed payload with the same key is a conflict. Store the operation key before sending. Stateless pair matching is new computation on a new operation; it creates no resolve receipt. Exact-content resolve replay is separately reported and free. Read the response's `usage` and evaluation state. Shallow chargeable results cost one credit; deep chargeable results cost five. Abstentions consume no credits but can consume deep attempts and provider budget. A 1,000-credit evaluation is not 1,000 chargeable research results. Trial access remains [planned](/api/pricing); no card, automatic purchase or hidden downgrade is promised. ## Long-running work and progress The shipped schema includes durable batch jobs: 1. Submit `POST /v1/batch-jobs` with the required `Idempotency-Key` and a body matching OpenAPI. A `202` acceptance gives a `Location` to poll; it is not a completed match or enrichment result. 2. Read `GET /v1/batch-jobs/{id}` for state and lifecycle progress. State names are progress evidence; do not turn them into an invented percentage. 3. Read `GET /v1/batch-jobs/{id}/result` after successful completion. Consume `GET /v1/batch-jobs/{id}/events` as SSE for events; reconnect with `Last-Event-ID` to resume from your last processed event. 4. Handle `queued`, `running`, `succeeded`, `failed`, `cancelled`, `erased` and `reconciliation_required` separately. An unresolved execution commitment requires reconciliation, not blind resubmission under a fresh key. `POST /v1/batch-jobs/{id}/cancel` cancels queued work; execution already running cannot be recalled. `/result` returns `202` with `Retry-After` while queued/running, `200` with the completed batch, or an error for a terminal state with no result. Erased results return `410`; completed failures do not become successful empty batches. An SSE event does not replace the authoritative result or receipt. Example submission (computation can consume credits), followed by polling: ```bash : "${RESOLVD_API_ORIGIN:?Set the operator's API origin}" : "${RESOLVD_API_KEY:?Set your issued credential}" curl --fail-with-body -i "$RESOLVD_API_ORIGIN/v1/batch-jobs" \ -H "Authorization: Bearer $RESOLVD_API_KEY" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: own-batch-evaluation-1' \ --data '{"records":[{"name":"INSTITUTE OF PATENTEES & INVENTORS","country":"GB", "registration_number":"00170887","jurisdiction":"GB","depth":"research"}]}' # Set the returned job_id. Honor Retry-After; do not poll in a tight loop. : "${RESOLVD_JOB_ID:?Set the returned job_id}" curl --fail-with-body "$RESOLVD_API_ORIGIN/v1/batch-jobs/$RESOLVD_JOB_ID" \ -H "Authorization: Bearer $RESOLVD_API_KEY" curl --fail-with-body -N "$RESOLVD_API_ORIGIN/v1/batch-jobs/$RESOLVD_JOB_ID/events" \ -H "Authorization: Bearer $RESOLVD_API_KEY" \ -H 'Accept: text/event-stream' -H 'Last-Event-ID: 0' ``` Persist the last processed event ID before reconnecting; `0` requests the initial history. Connections are finite and may end before the job does. Workers recheck credentials before execution; revoked credentials cannot start queued work. The operator must run `python -m resolvd.worker` with the same database/configuration as the API. `--once` processes a bounded worker tick. A stopped worker leaves jobs queued; accepting a job does not prove a worker is healthy. ## Signed completion webhooks Register `POST /v1/webhooks` with a required `Idempotency-Key`, your public HTTPS port-443 `url` and `event_types` from OpenAPI. This requires resolve permission; reading subscriptions/delivery receipts accepts read permission. Private/local destinations and redirects are refused. Store the returned `whsec_` signing secret in your receiver's secret store. An idempotent creation retry returns the same subscription/secret; list responses omit secrets. Notifications contain a job ID/state, not organization records or credentials. Retrieve the result using your own authenticated API call. The supported events are the terminal `batch.succeeded`, `batch.failed`, `batch.cancelled`, `batch.reconciliation_required` and `batch.erased` states. Verify Standard Webhooks headers `webhook-id`, `webhook-timestamp` and `webhook-signature`. Base64-decode the secret after removing `whsec_`; compute HMAC-SHA256 over `webhook-id + '.' + webhook-timestamp + '.' + exact_body_bytes`. Compare its base64 value with the `v1,` signature using constant-time comparison. Reject timestamps outside a five-minute tolerance before accepting the event. Verify the exact received bytes before parsing or reserializing JSON. Persist/deduplicate `webhook-id`, acknowledge with a `2xx` response, and process asynchronously. At most five attempts use delays of 5, 30, 120 and 600 seconds; retries keep the same event ID. Inspect `GET /v1/webhooks/{id}/deliveries` for the latest 100 delivery receipts, including final `failed` state. A delivery failure does not change the job result; polling remains available. `DELETE /v1/webhooks/{id}` disables delivery, but cannot recall a request already in flight. The operator must configure `RESOLVD_WEBHOOK_SIGNING_KEY` (at least 32 characters) and run the shared worker. There is no default key. Existing subscriptions pin the master-key fingerprint; changing the key requires explicit operator recovery. ## Company fields and local changes Discover fields and named packs at `GET /v1/reference/enrichment-fields` or MCP `get_enrichment_fields`. `POST /v1/resolve`, `GET /v1/entities/{entity_id}` and `GET /v1/entities/{entity_id}/intelligence` accept `fields`, `field_packs` and `include_field_status`. For GET requests, repeat a query parameter for each selected name, for example `?fields=identity.legal_name&fields=financials.revenue`. These selectors only project returned values and provenance; they do not limit source acquisition, change evidence or reduce computation charges. The default with neither selector remains the complete authorized view. Explicit selections include `field_status`; `include_field_status` requests diagnostics for the complete view, and intelligence reads default to diagnostics. Missing values stay separate from `field_status`; no null is replaced with a diagnostic object and no missing amount becomes zero. `not_checked` is not a company-specific absence finding. Capability readiness and resale entitlement are also separate from company coverage and fact-level licences. Unknown financial entity/group scope remains `unknown`. `GET /v1/entities/{entity_id}/events` and MCP `get_entity_events` accept repeated `categories` names such as `financials`, `ownership` or `legal_status`. Consume `next_cursor` even when filtering returns no events. `scanned_count` is the number scanned before filtering; a full scanned page can have later matches. These events reflect committed local watch observations, not a live provider monitoring service. Read values and their citations through `/intelligence`. ## Company workflows with retained local evidence These October 7 additions are implemented locally. The repository records development validation separately from deployment. Production is unchanged on schema `0029`. Development migration `0030` requires coordinated API/worker rollout. The running OpenAPI/tool catalogue states which contracts this deployment actually serves. | Workflow | Primary REST paths under `/v1` | |---|---| | Portfolios | `/portfolios`, `/portfolios/{portfolio_id}` and child routes | | Financial statements | `GET /entities/{entity_id}/financial-statements` | | Identifier lifecycle | `GET /entities/{entity_id}/identifier-history` | | Source disagreement | `POST /entities/{entity_id}/source-comparison` | | Typed local queries | `GET /reference/search-fields`; `POST /entities/query` | | Cited relationship paths | `POST /relationships/explain` | | Data-selection receipts | `POST /data-selections`; `/data-selections/{receipt_id}` | Equivalent registered MCP tools: - Portfolios: `manage_portfolio`, `list_portfolios`, `get_portfolio`, `manage_portfolio_member`, `get_portfolio_members`, `get_portfolio_events`, `enable_portfolio_watches`, `export_portfolio`. - Statements: `get_financial_statements`, `select_financial_statement`, `compare_financial_statements`. - Identifier lifecycle: `get_identifier_history`. - Source disagreement: `compare_entity_sources`. - Typed queries: `get_search_fields`, `query_companies`. - Relationships: `explain_relationship_paths`. - Receipts: `capture_data_selection`, `get_data_selection`, `delete_data_selection`. Use company IDs already owned by the authenticated tenant. Reads use retained facts/configured authorized local observations, with no provider or model calls. Remote `read` or `resolve` credentials can read; list edits, receipt creation and deletion require `resolve`. Optional `capture: true` also requires that scope. Ordinary reads save no receipt. These workflows make no company-match decision or source-fact correction. Discover filter operators with `get_search_fields` or the REST reference route. All query filters combine with AND. `identity.legal_name` supports `eq`, `contains`, `in`; `digital.domain` and `location.country` support `eq`, `in`. `entity_id` supports `eq`, `in`, and `created_at` supports `gte`, `lte`. Only `created_at`/`entity_id` are sortable, with `asc`/`desc` directions and an entity-ID tie-break. `UK` aliases to `GB`; date-times need timezones and must be UTC-representable. Returnable enrichment fields are not automatically searchable. For `POST /v1/entities/query`, this is the body; MCP `query_companies` takes it as `payload`: ```json { "filters": [{"field":"location.country","operator":"in","value":["GB","US"]}], "sort_by":"created_at","direction":"asc","limit":50,"scan_limit":500 } ``` Bounds are 16 filters, 50 strings per `in`, 100 matches and 1,000 scanned rows per request. Read `scanned_count` and follow `next_cursor` even through empty filtered pages, keeping the same filters/sort. Stop when it is null. The cursor fixes tenant/query and creation-time boundary; corrections/deletions can still change values. Source-effective `as_of` is not a complete knowledge snapshot. Financial selection takes a `selection` object with optional sibling `capture`. The exact wrapper is shared by REST and MCP. This MCP call only reads: ```json { "jsonrpc":"2.0","id":6,"method":"tools/call", "params":{ "name":"select_financial_statement", "arguments":{ "entity_id":"ent_from_your_own_resolve_result", "payload":{ "selection":{ "relative_period":0, "requirements":{"attributes":["financials.revenue"]}, "preferences":{"currency":"GBP","scope":"consolidated_group"} }, "capture":false } } } } ``` REST `POST /v1/entities/{entity_id}/financial-selection` takes that `payload` object as its body. Use exact `statement_id`, exact ISO `period_end`, or integer `relative_period` 0 to 20. Relative zero means the newest distinct available reporting end after strict requirements, not a calendar-year offset. Exact and relative periods cannot be combined. Both criteria objects support `source_id`, uppercase three-letter `currency`, `scope`, `basis` and exact financial `attributes`. Scope is `legal_entity`, `consolidated_group` or `unknown`; basis is `reported`, `derived`, `estimated`, `average`, `point_in_time` or `unknown`. Requirements never fall back; unavailable strict choices return `selected: null` and a gap. Preferences choose the fewest mismatches, then latest dates/stable ID. Read `requested`, `selected`, `actual`, `fallback_reasons` and `gaps` together. No FX conversion or consolidation inference occurs. The inventory returns stable IDs, source fingerprints, cited observations and truncation/coverage gaps. Archives retain admitted values already acquired, not all upstream filings. POST `/v1/entities/{entity_id}/financial-comparison` with `left_statement_id` and `right_statement_id`; the response keeps both cited statements, `inventory_gaps`, `inventory_truncated`, `inventory_note` and comparison gaps. A changed amount or amendment is not proof of restatement; explicit source restatement status and an earlier-accession link are required. Portfolios allow 100 lists per tenant and 1,000 owned companies per list. Create with `POST /v1/portfolios` and a name/optional stable `portfolio_id`; rename with PATCH, delete with DELETE. PUT/DELETE `/members/{entity_id}` maintain membership, with optional `customer_reference`/`role` as list metadata, not identity evidence. List/member/export pages have at most 100 entries. Watch enabling accepts `interval_seconds` 300 to 86400, `limit` and `cursor`; follow returned pages. At most 1,000 entity watches exist per tenant. Re-enabling preserves baselines; watches are shared across lists. Removing a list/member leaves company data and shared watches intact. Events expose local observation changes and advancing cursors/scanned counts, not complete provider history. Export pages contain current local values/citations and are not a frozen multi-page snapshot. Identifier history keeps sourced validity/successor evidence and paged durable local correction events separate. A provider removal is not dissolution and there is no automatic ID redirect. Source comparison takes `fields`, optional `include_history` and `capture`; it returns dated observations/survivor policy, disagreement, bounds, `withheld_by_licence` and `coverage_complete: false`. Future/pass-through values are excluded. Relationship explanation takes two owned entity IDs, optional `as_of`, `max_hops` 1 to 10, `limit` 1 to 20 and `capture`. Edges are cited and dated; share ownership, control and accounting consolidation stay distinct. Missing paths do not prove absence or shared legal identity. Set `capture: true` on financial selection, source comparison or relationships, or POST `/v1/data-selections` with `entity_ids`, optional `fields`, `field_packs` and `as_of`. Limits are 1,000 receipts per tenant, 100 companies, 5,000 contributing customer-record references and 2,000,000 UTF-8 JSON bytes per capture. Current projections refresh authorized local feeds; historical captures use retained stored facts, without replaying a complete old feed or prior knowledge state. Pass-through values never enter a saved capture. Receipts fix values, citations, source/evaluation dates, policy and fallback/gaps. Fingerprints normalize caller resale flags to false, as stated by `fingerprint_scope`; receipt retrieval derives current eligibility from captured licences and the current caller plan, preserving raw values. No supplier licence is acquired or refreshed. Entity, contributing customer-record and captured intermediary erasure purge the entire receipt. GET `/v1/data-selections/{receipt_id}` to reopen it; DELETE that path to remove the capture without changing company facts. Keep its reference to reopen/download it. For safe response-loss retries, supply a stable client-generated `capture_id` in the projection request or beside `capture` in a financial/source/relationship wrapper. This optional ID accepts ASCII `[A-Za-z0-9_-]{1,64}` and becomes `receipt_id`; otherwise it remains a generated `ds_`. Within the same tenant, the same ID and workflow/input intent return the original immutable receipt/snapshot with current caller redistribution rights. Changed intent returns HTTP `409`. Replay does not refresh observations or call remote providers. Deleting/erasing the receipt keeps that ID reserved; it cannot recreate the capture. Retry returns `409`, or company access checks may return `404` first. Use a new ID for a new capture. The ledger retains only tenant ID and SHA-256 token/intent hashes, with no plaintext token, company IDs or evidence. There are at most 10,000 explicit capture-ID reservations per tenant; the live receipt limit remains 1,000. The workspace exposes these operations through authenticated `/portal/api` routes. No Orbis dataset/access, global lifecycle database, complete upstream history, CRM writeback or new accuracy claim is implied. ## Company-only reference batches without AI `POST /v1/company-reference/batch` and MCP `lookup_company_reference_batch` read the existing licensed Tenhaw company warehouse. They do not invoke company resolution, models, public registries or metered commercial enrichment APIs. The workspace Company lookup view also accepts a mapped UTF-8 CSV and exports the returned profiles. This workflow consumes zero resolution credits. The request takes `records` (1-100 rows) and `limit` (2-10 candidates per row, default 5). Each row accepts only `client_ref`, `domain` and optional US/GB `country`; UK aliases to GB. Unknown input columns are not imported. Example: ```json {"records":[{"client_ref":"crm-1","domain":"example.com","country":"GB"}],"limit":5} ``` For MCP put this object in `arguments.payload`. Each response row preserves its one-based `row_number` and original input. Status is `candidates`, `no_candidates`, `unavailable`, `missing_domain` or `invalid_input`. Unavailable results carry a fixed reason; they do not imply the company does not exist. Every competing profile up to the cap is retained, with explicit truncation and licensed, unverified source snapshot provenance. A profile is not a verified legal match; snapshot extraction dates do not establish field freshness. No profiles are automatically selected or written to a tenant entity/decision store. One batch admission and one ordinary tenant READ admission per distinct valid normalized domain/country are required before any warehouse lookup. Insufficient read allowance rejects the batch with rate_limited before source work. Duplicate rows share one lookup within a request but remain separate output rows. At most four warehouse queries run concurrently; a 30-second execution budget returns completed rows and explicitly labels unfinished rows unavailable/request_deadline. Each individual source read retains its existing five-second maximum budget. ## Evaluate evidence, not response volume Check decision/confidence basis, identity authority and each field's source, reporting period, currency and legal-entity/group scope. A populated employee field for the wrong group or period is not verified enrichment. Preserve errors, abstentions, unknown truth and budget interruptions in the denominator. [Dated examples](/api/demo) and [evidence methodology](/api/evidence) state their limits; neither is a live demonstration or proof of 99% accuracy.