Integration reference: APIs, data contracts & events¶
This is the developer/integrator reference for connecting an external system to tigerdistribute — a Policy Administration System (PAS), a producer’s own feed, or a downstream event subscriber. It documents the HTTP endpoints, the machine credentials, the canonical data contracts, the outbound event catalog and the adapter framework.
The user-facing behaviour of these features (the Ingestion screen, currency gaps, managing webhook endpoints) lives in Integrations: PAS data & webhooks; the background jobs that drive delivery are described in Operations: background jobs & schedules. This page is for the engineer building the integration.
The integration boundary¶
tigerdistribute does not own policies, quotes or billing — those live in a PAS. The platform integrates to do two things:
Ingest facts — bound / endorsed / cancelled policy rows and commission transactions, which drive
performance,targetsandcommissions.Emit status — producer, appointment, payability and payout domain events, which a subscriber consumes to keep its own copy in sync and to settle.
No PAS is special. Every system — Guidewire, Sapiens, Socotra, isotope or a carrier’s in-house platform — integrates by conforming to the same canonical contract. Their differences are absorbed by a thin coded adapter per system; the contract itself never names a vendor. isotope is the reference adapter, not a privileged one.
ANY PAS ADAPTER (per PAS) CANONICAL CORE
┌──────────┐ push/file/ ┌───────────────────┐ ┌──────────────────┐
│ Guidewire│ pull/bus │ GuidewireConnector │ → │ PolicyFact │
│ Sapiens │ ──────────────► │ SapiensConnector │ → │ CommissionFact │ → performance
│ Socotra │ │ SocotraConnector │ │ (IngestionBatch)│ targets
│ isotope │ ◄────────────── │ IsotopeConnector │ ← │ Outbox events │ commissions
└──────────┘ status events └───────────────────┘ └──────────────────┘
Every ingress mode converges on one normalized output
(ConnectorResult(policies=[…], commissions=[…])); from there the
persistence and idempotency path is identical, so the core is
mode-agnostic. The adapter’s only job is translation.
Topology: ingress and egress¶
Every mode lands the same canonical facts in the same IngestionBatch
pipeline.
Mode |
Direction |
Initiator |
When to use |
|---|---|---|---|
Push |
PAS → DMS |
PAS |
PAS has outbound webhooks / an API (modern systems). |
File |
PAS → DMS |
PAS / ops |
Bordereaux (CSV / XLSX). The universal fallback — anything can export a file. |
Pull |
DMS → PAS |
DMS |
PAS has a query API but no outbound events (older systems). |
Bus |
PAS → DMS |
PAS |
Carrier runs an event backbone (Redis Streams today; Kafka is a future transport). |
Status (egress) |
DMS → PAS |
DMS |
Every subscriber that gates on producer / appointment status or consumes payouts. |
Authentication¶
The REST API mounts under /api/. There is no URL or header API
versioning — the surface is flat and evolves additively (see
Versioning & compatibility). Interactive clients (the React SPA) use
session auth; machine callers use one of three purpose-built credentials.
Tenancy¶
Every request resolves to exactly one tenant, and row-level security is
enforced in the database. Most machine credentials require an explicit
X-Tenant: <slug> header; the PAS push token is the exception — it
self-identifies its tenant, so no header is needed.
Producer API key¶
For a producer pushing its own policy and commission rows.
Header:
Authorization: Api-Key tdk_<prefix>_<secret>plusX-Tenant: <slug>.Every ingested row is forcibly attributed to the key’s producer.
Throttled at 120 requests/hour per key.
Keys are minted, rotated and revoked through
/api/producer-api-keys/by a tenant manager; the secret is shown once on create/rotate.
PAS push token¶
For a PAS delivering webhooks to the universal receiver.
A self-identifying opaque token
tdp_<prefix>_<secret>whose globally unique prefix resolves thePASConnection— tenant and adapter — so the PAS needs noX-Tenantheader.Supply it in the URL (
/api/pas/ingest/<token>/) or asAuthorization: Bearer <token>— whichever the PAS’s webhook config allows. This is configuration, not PAS code.Minted per connection via
POST /api/pas-connections/<id>/mint-push-token/(returns the token and the push URL once); revoked withrevoke-push-token.
Signed webhook (outbound & inbound callbacks)¶
Outbound deliveries — and inbound provider callbacks such as screening — are
authenticated by an HMAC-SHA256 signature over the raw body, carried in
X-TigerDistribute-Signature: sha256=<hex>. Consumers must verify it before
trusting a delivery. See Outbound: domain events.
Inbound: ingesting policy & commission facts¶
Endpoints¶
Endpoint |
Auth |
Payload |
|---|---|---|
|
PAS push token |
Any shape — the connection’s adapter normalizes it. |
|
Tenant member + |
Canonical batch (staff / trusted callers). |
|
Producer |
Canonical batch; all rows forced to the key’s producer. |
|
Tenant member + |
Multipart CSV / XLSX (≤ 10 MB) + a mapping template. |
|
Tenant member + |
Canonical batch — dry-run, persists nothing. |
Ingested batches and their rows are readable at /api/ingestion-batches/,
/api/ingested-policies/ and /api/ingested-commissions/ (filter by
?batch=, ?unresolved=true, ?status=, ?source=).
The universal push receiver¶
/api/pas/ingest/ is the primary path for a PAS we cannot modify. It
makes no assumption about the payload: it captures the raw body, headers and
(when parseable) JSON into a PushEnvelope and hands it to the connection’s
adapter, which owns all interpretation — CloudEvents structured (JSON body) or
binary (ce-* headers), or a bespoke envelope.
Lenient. An event the adapter doesn’t consume yields an empty batch and a
202— a PAS webhook is never failed over something we can’t fix. Retries are safe because ingestion is idempotent on external ids.Optional hardening. A PAS that can sign or send a static secret may be additionally verified inside its adapter — not required.
Canonical inbound schema¶
Whatever the ingress mode, an adapter emits rows in these two shapes.
PolicyFact — maps to integrations.IngestedPolicy.
Field |
Type |
Req |
Notes |
|---|---|---|---|
|
|
✱ |
Producer external id (see Producer identity). On the wire today:
a |
|
string |
✔ |
The PAS’s stable policy id. Idempotency key. |
|
string |
— |
Human policy number. |
|
string |
— |
Maps to |
|
string |
— |
ISO / state code; global-first (never a bare “state” string). |
|
enum |
✔ |
|
|
enum |
— |
|
|
decimal(14,2) |
✔ |
Native-currency amount. |
|
ISO 4217 |
✔ |
Defaults to the tenant base; unknown codes rejected. A missing value is never guessed — the row is held in the batch’s currency-gap queue. |
|
date |
— |
Original policy issue date, carried unchanged across renewals — the
anchor for years-in-force rate bands. New business: equals
|
|
date |
✔ |
Effective date of this transaction (a renewal has its own). |
|
date |
— |
|
|
date |
— |
Snapshot date; part of the idempotency key. |
Idempotency: unique on (tenant, external_policy_id, transaction_type,
as_of_date). Re-sending the same tuple updates the row — safe to replay.
CommissionFact — maps to integrations.IngestedCommissionTxn.
Field |
Type |
Req |
Notes |
|---|---|---|---|
|
|
✱ |
As above. |
|
string |
✔ |
The PAS’s stable txn id. Idempotency key. |
|
string |
— |
Links the commission to a policy. |
|
enum |
✔ |
|
|
decimal(14,2) |
✔ |
Signed; a chargeback is a negative earning. |
|
ISO 4217 |
✔ |
Native currency; a batch may freely mix currencies. |
|
date |
✔ |
Idempotency: unique on (tenant, external_txn_id).
Note
A CommissionFact is the amount as booked by the PAS’s billing — the
settled figure, never a rate. tigerdistribute determines the authoritative
commission from its own plans; CommissionReconciliation ties the two
together. The tenant’s commission feed setting decides whether ingested
commissions are verified against the plan or the platform calculates from
ingested policies (see Commissions: plans, statements, splits & payouts).
Batch envelope¶
The canonical batch shape (push & file), what /api/ingestion-batches/ingest/
and /api/producer-ingest/ accept:
{
"source": "guidewire-pc",
"external_reference": "nightly-2026-07-08",
"policies": [ /* PolicyFact[] */ ],
"commissions": [ /* CommissionFact[] */ ]
}
source— free-text feed label (the adapter sets it).external_reference— the sender’s batch id; re-sending updates the batch rather than duplicating it (batch-level idempotency).Response — an
IngestionBatchwithpolicy_count,commission_countandunresolved_count.
Producer identity¶
A fact names its producer with a (scheme, value) external identifier, not a
bare number — this is what makes the platform global.
Scheme |
Meaning |
Registry |
|---|---|---|
|
US National Producer Number |
NIPR / PDB |
|
UK FCA firm / individual reference |
FCA |
|
Tenant-internal |
— |
A fact whose identifier matches no producer is kept, never dropped — stored with a null producer and surfaced in the batch’s unresolved queue for an operator to bind later.
Note
The wire and resolver are NPN-only today (a producer_npn column;
Producer.objects.filter(npn=…)). Generalizing to (scheme, value) is
planned work; the schema above shows the target form with the current
NPN-only form noted inline.
File ingress (bordereaux)¶
POST /api/bordereaux-uploads/ accepts a multipart CSV / XLSX (≤ 10 MB). The
upload is parsed, its detected headers and a sample of rows returned, and a
BordereauxMappingTemplate (source column → canonical field, managed at
/api/bordereaux-templates/) maps it onto the canonical schema before it
enters the same ingestion pipeline.
Stream ingress (event bus)¶
When a carrier runs an event backbone, a connection consumes it directly. The
transport layer is pluggable behind a small read / ack interface:
Redis Streams is the implemented transport — a consumer group (
tigerdistribute), at-least-once semantics, one stream per connection (PASConnection.bus_topic), each entry a JSON body the adapter’sconsume_messagenormalizes.Kafka is a declared future transport, not yet built.
Run the consumer with manage.py run_pas_bus_consumer. A message left
unacked (e.g. mid-crash) is redelivered — never dropped.
Dry-run validation¶
POST /api/ingestion-batches/validate/ runs a batch through the full
normalize/resolve path and reports row counts, unresolved identifiers and
currency gaps without persisting anything — the conformance check a new
integration proves itself against.
Outbound: domain events¶
When something notable happens, tigerdistribute records an outbox event and delivers it to subscribers. Delivery uses a transactional outbox so an event is never lost if a downstream is briefly unavailable.
Envelope & headers¶
Each event is POSTed as a signed JSON envelope:
{
"id": "c1b2a3d4-…",
"event_type": "ProducerOnboardedV1",
"aggregate_type": "producer",
"aggregate_id": "a2d4f6e8-…",
"occurred_at": "2026-07-08T02:05:00Z",
"payload": { }
}
Header |
Purpose |
|---|---|
|
The event type. |
|
The event id — subscribers dedupe on this. |
|
|
Delivery semantics¶
At-least-once per endpoint — the same event can arrive more than once; dedupe on the delivery id.
An event is marked published only once every active endpoint has accepted it.
On failure, exponential backoff (60 s base, 1 h cap) over 8 attempts, then dead-letter with a manual retry.
With no active endpoints, events wait as pending — nothing is lost.
Register subscriber URLs at /api/webhook-endpoints/ (the signing secret
is write-only). Inspect and re-drive the queue at /api/outbox-events/
(relay/ for an immediate sweep, <id>/retry/ to requeue a dead letter).
Event catalog¶
All of the following are emitted today.
Event |
Aggregate |
Meaning |
|---|---|---|
|
producer |
Producer activated; carries the syncable identity. |
|
producer |
Name / identifier / status changed. |
|
producer |
Producer ended. |
|
contract |
Producer agreement executed. |
|
appointment |
Producer may place in a jurisdiction / company. |
|
appointment |
Appointment ended. |
|
producer |
A product-authorization change altered the can-place verdict. |
|
producer |
Commission hold / release (affects agency-bill netting). |
|
payout |
A payment run produced an instruction (see below). |
|
producer |
A screening result arrived. |
Egress mechanisms¶
The same outbox feeds two independent delivery paths; a connection chooses per tenant:
Webhook relay — a signed HTTP POST to every active
WebhookEndpoint. This is how a generic subscriber (including a downstream billing/AP system) receives events.PAS API-push — for a PAS whose own API should be called, the connection’s adapter translates the canonical event into that PAS’s native API calls. Delivery state is tracked per
(connection, event)independently of the webhook relay, with the same retry/backoff. An adapter declares which event types it applies viaegress_event_types(); every other event is skipped for that connection (never a failed delivery).
Payout instructions¶
A payment run consolidates everything payable for a producer into one
PayoutInstruction per (producer, currency) pot, nets same-currency debt
against the ledger, applies the minimum-payout threshold, and publishes
PayoutInstructedV1. tigerdistribute instructs; it does not move money —
a downstream system disburses exactly the net amount and confirms back.
The payload carries the instruction id, producer, run date, currency, gross,
recovery applied, net amount, and a sources array — one entry per
statement or award consolidated (id, amount, period, and for awards the program
name). Subscribe, disburse the net, and dedupe on the instruction id.
The return leg is exposed on the payout API for the disbursing system’s status
callbacks: acknowledge (received), mark-paid (money went out — records
an external_reference) and release (publish a previously held
instruction). Statuses flow held → pending → published → acknowledged →
paid.
Writing a PAS adapter¶
Each PAS gets a first-class PASConnector subclass under
integrations/connectors/. The base class defines the normative interface;
an adapter implements only the modes it declares in capabilities() — every
other method stays NotImplementedError and is never invoked.
The connector interface¶
class PASConnector:
source: str # unique key: "isotope", "guidewire", …
def capabilities(self) -> set[str]:
# subset of {"push", "file", "pull", "bus",
# "push_status", "webhook_status"}
...
# ---- ingress: each mode → canonical facts ----
def normalize_push(self, envelope) -> ConnectorResult: ...
def parse_file(self, name, data) -> ConnectorResult: ...
def pull(self, *, since=None) -> ConnectorResult: ...
def consume_message(self, message) -> ConnectorResult: ...
# ---- egress: apply canonical status via the PAS's own API ----
def egress_event_types(self) -> set[str]: ...
def push(self, connection, event_type, payload) -> bool: ...
Every ingress method returns the same ConnectorResult(policies=[…],
commissions=[…]). push() returns True when the PAS consumed the event,
False when it isn’t one this PAS applies (a no-op, not a failure).
Registering & configuring¶
Decorate the class with @register_connector and load its module in
connectors/__init__.py; it is then available by its source key through
the registry. A tenant wires it up as a PASConnection (/api/pas-connections/):
choose the source, enable the ingress modes (gated so a mode is on only if
both the tenant enables it and the adapter declares it), pick the
egress_mode, and store encrypted credentials. adapters lists what’s
available; test probes connectivity; sync pulls deltas.
Bundled adapters¶
Source |
Status |
Declared capabilities |
|---|---|---|
|
Reference adapter (full) |
push · file · pull · push_status · webhook_status |
|
Stub |
pull · file |
|
Stub |
file · pull |
|
Stub |
push · bus · webhook_status |
|
Stub |
file · pull |
Stubs declare their typical integration shape but do not yet translate payloads — they are the starting point for a real adapter.
Versioning & compatibility¶
Events are versioned in the type name (
…V1). A breaking change ships as…V2; both run in parallel until subscribers migrate.Inbound schema is additive-only within v1 — a new optional field never breaks a sender. A breaking change bumps the contract major.
Identifier schemes are open-ended — adding
FCA/CODE/ others is not a breaking change.The REST surface itself is unversioned in the URL (flat
/api/); rely on the additive discipline above rather than a/v1/prefix.
Conformance checklist¶
An integration is “done” when, against a sandbox tenant, it can:
Ingest a PolicyFact + CommissionFact batch (any mode) and see counts with zero unexpected unresolved rows.
Resolve identity — its producer identifiers match tigerdistribute producers (or are surfaced as unresolved and bindable).
Receive & verify a signed webhook envelope (signature valid; dedupes on the delivery id).
Reconcile — its commission facts tie to a statement via
CommissionReconciliation.(If gating) consume
AppointmentConfirmedV1/MarketAccessChangedV1and gate binding on them.
Use POST /api/ingestion-batches/validate/ to prove step 1 without
persisting.
The live OpenAPI schema¶
The inbound REST surface is described by an auto-generated OpenAPI schema (drf-spectacular):
/api/schema/— the raw schema./api/docs/— Swagger UI./api/redoc/— ReDoc.
Note
These are currently served to admin users only. Relaxing the serve permission for a sandbox environment is what lets an external integrator self-serve the endpoint reference.
Where the code lives¶
Concern |
Module |
|---|---|
Canonical inbound models |
|
Ingestion pipeline |
|
Inbound API |
|
Adapter base & registry |
|
Reference adapter |
|
Bus transport |
|
Outbound publish |
|
Outbound relay & egress |
|
Payout / settlement |
|
Durable orchestration |
|