Partner API v1 · Draft, not yet live

Partner API v1: overview

Draft — not yet live. This describes the proposed v1 partner contract (version 1.0.0-rc.1). No endpoint on this page is deployed, and no API key can be issued yet.

Version 1 is the partner-facing layer over the Crown Intelligence data service. It adds API keys with scopes, metering, batch matching with a clear status for every query, webhooks, monitoring of public list entries and evidence packs. This page explains the rules that every operation follows.

Status of this API

The v1 contract is a draft, version 1.0.0-rc.1. It is not deployed, not published and not billed. The addresses below are proposed; they do not resolve to a running service yet.

Proposed base addresses for Partner API v1
Live (proposed)https://api.crownintelligence.net
Sandbox (proposed)https://sandbox.api.crownintelligence.net, the same public registers, never billed
Machine-readable contractcrown-intelligence-v1.yaml (OpenAPI 3.1.0, draft)

API keys and scopes

Send the key in the X-Api-Key header on every request except GET /v1/status, which needs no key.

  • Live keys start with ci_live_. Sandbox keys start with ci_test_. A sandbox key never reaches a live route and never creates a billable order.
  • The secret is shown once, when the key is created. Crown Intelligence keeps only a hash and the visible prefix.
  • Each key carries explicit scopes. A key without the scope a route needs gets 403 forbidden_scope.
  • Revocation is immediate and final. A revoked key returns 401 key_revoked. Keys are retired, never deleted.
  • A live key must name the subscription order it is billed under (order_id).
API scopes and what each one covers
ScopeCovers
match:readPOST /v1/match, and reconciliation
search:readSearch, records, entities and collections
sources:readSources, programmes and collection statistics
exports:readRegister downloads, where the licence allows
webhooks:manageWebhook endpoints and their delivery logs
monitors:manageMonitors of public list entries
cases:read, cases:writeCases, dispositions and evidence packs
usage:readUsage in units
keys:manageCreating and revoking keys

Try it: a keyed request

curl:

curl https://api.crownintelligence.net/v1/sources \
  -H "X-Api-Key: $CI_API_KEY"

Node (20 or later):

const res = await fetch("https://api.crownintelligence.net/v1/sources", {
  headers: { "X-Api-Key": process.env.CI_API_KEY }
});
if (!res.ok) throw new Error("HTTP " + res.status);
const body = await res.json();

Health check without a key:

curl https://api.crownintelligence.net/v1/status

Billing reference: internal calls only

Every cross-service call carries a billing reference, sent in the X-Billing-Ref header as compact ASCII JSON of at most 1,024 bytes. It records who the call is for, so that usage can be billed and cost recorded.

{"source_service":"partner-api","source_account":"acct_demo_bank",
 "order_id":"ord_2026_1041","type":"ORDER","parent_call":null,"invoice_line_id":null}
  • Partner calls may leave the header out. The gateway fills source_service and source_account from the authenticated key, and takes the order from the key's order_id. A partner cannot set these itself. If a sent header disagrees with its key, the call is refused with 400 billing_ref_invalid.
  • Internal calls must send it. A Crown service calling Crown Intelligence without the header is refused with 400 billing_ref_missing. The object passes down a chain unchanged, and each hop sets parent_call to the call that triggered it.
  • type is ORDER when a customer pays for the call. It is OVERHEAD for system traffic, with no invoice and cost still recorded. An ORDER must carry order_id; an OVERHEAD carries order_id: null.
  • No other fields are allowed. The API never shows money: usage is reported in units only, and prices are set on the invoice.
// Internal Crown service calling match: pass the caller's billing_ref unchanged in X-Billing-Ref.
const billingRef = {
  source_service: "your-registered-service-id",
  source_account: null,
  order_id: null,
  type: "OVERHEAD",
  parent_call: incomingBillingRefId,   // id of the call that triggered this one, or null at the top
  invoice_line_id: null
};
const res = await fetch("https://api.crownintelligence.net/v1/match", {
  method: "POST",
  headers: {
    "X-Api-Key": process.env.CI_INTERNAL_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    "X-Billing-Ref": JSON.stringify(billingRef)
  },
  body: JSON.stringify({ queries: [{ query_ref: "svc-1", entity_type: "Company", name: "Northwind Trading LLC" }] })
});

Open items: the source identifier partner-api is not yet registered in the service catalogue, and the owner has not yet decided whether usage rows carry a price reference. See the contract, sections 4 and 16.

Match statuses: never clear by default

Every query in a match response has one of four statuses. The rule is simple: a query is reported as clear only when every register it should cover was searched and nothing matched. The word clear is not a value anywhere in the API.

The four match statuses
StatusMeaningCan you treat it as no match?
hitAt least one match at or above min_bandNo. Review the matches.
no_hit_searchedEvery listed register was searched. Nothing matched. The response lists each register with loaded_at.Yes. This is the only negative outcome.
not_searchedNothing was checked, for example an identifier no loaded register covers. The reason is given.Never. Not checked is not clear.
errorThe check failed. Retry it.Never. A failed check is not clear.

A status of not_searched or error must never be shown as clear, stored as clear or passed on as clear. Build your decision logic so that only no_hit_searched can ever produce a negative result.

Match bands

Each match carries a band instead of a raw threshold. Bands are derived from the score (0 to 1) and the identifiers that agree.

Match bands and their rules
BandRuleReturned when
STRONGScore 0.90 or more, or an exact IMO, MMSI or LEIAlways, if at or above min_band
POSSIBLEScore 0.75 to 0.89The default min_band
WEAKScore 0.60 to 0.74Only when min_band is WEAK

Nothing below 0.60 is returned. The band floors are proposed, not yet calibrated against a labelled set of pairs, so do not build a fixed rule on them until calibration is published. Matcher versions will be exposed on each response.

Rate limits and quotas

These defaults are proposed. Plan limits are placeholders until the owner sets them.

Proposed rate limits
LimitProposed default
Requests per minute, per key120
Query units per minute, per key600
Queries per batch100
Export requests per minute, per key10
Monthly unitsSet per plan. Exceeding it returns 429 quota_exceeded.

Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. Metered calls also carry X-Usage-Units. A 429 response carries Retry-After.

Idempotency and versions

Every POST accepts an Idempotency-Key of up to 255 characters. Repeating the same key with the same body within 24 hours returns the stored response with no second charge. Reusing the key with a different body returns 409 idempotency_mismatch.

The version is in the path (/v1). Changes within v1 are additive. A breaking change moves to /v2, with at least six months' notice of any deprecation.

Errors

Every error is application/problem+json (RFC 9457). Each error carries a stable code, a request_id to quote to support, and its message in English (messages.en) and French (messages.fr).

{"type":"https://api.crownintelligence.net/problems/invalid_request","title":"Invalid request",
 "status":400,"code":"invalid_request","detail":"queries[3].name must be at least 2 characters",
 "messages":{"en":"queries[3].name must be at least 2 characters",
   "fr":"queries[3].name doit comporter au moins 2 caractères"},
 "request_id":"req_01J9Z3K4M6","errors":[{"pointer":"/queries/3/name","code":"too_short",
   "message":{"en":"Use at least 2 characters.","fr":"Utilisez au moins 2 caractères."}}]}
Common error codes
HTTPCodeWhen
400invalid_requestThe body or a parameter fails the schema
400billing_ref_missing, billing_ref_invalidAn internal call has no billing reference, or its fields disagree with the key
400personal_data_not_acceptedA monitor body carries a personal field
401unauthenticated, key_revokedThe key is missing, unknown or revoked
403forbidden_scope, forbidden_environmentThe scope is missing, or a sandbox key calls a live route
404not_foundAlso used where a record is not visible to the account
409conflict, idempotency_mismatchA duplicate, or a reused Idempotency-Key with a different body
413payload_too_large, batch_too_largeOver 1 MB, or more than 100 queries
429rate_limited, quota_exceededSee rate limits above. Wait for Retry-After.
503register_unavailable, unavailableA register or the service is down. Retry with backoff.
500internal_errorReport it with the request_id

In a batch, a single query can fail with an error status. The batch itself still returns 200, and that query is never reported as clear.

const res = await fetch(url, { headers: { "X-Api-Key": process.env.CI_API_KEY } });
if (!res.ok) {
  const problem = await res.json();              // application/problem+json
  console.error(problem.code, problem.request_id, problem.messages.en);
  if (res.status === 429 || res.status === 503) {
    const wait = Number(res.headers.get("Retry-After") ?? 5);   // back off, then retry the same request
  }
}

Pages in this section