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, targets and commissions.

  • 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> plus X-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 the PASConnection — tenant and adapter — so the PAS needs no X-Tenant header.

  • Supply it in the URL (/api/pas/ingest/<token>/) or as Authorization: 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 with revoke-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

POST /api/pas/ingest/ POST /api/pas/ingest/<token>/

PAS push token

Any shape — the connection’s adapter normalizes it.

POST /api/ingestion-batches/ingest/

Tenant member + X-Tenant

Canonical batch (staff / trusted callers).

POST /api/producer-ingest/

Producer Api-Key

Canonical batch; all rows forced to the key’s producer.

POST /api/bordereaux-uploads/

Tenant member + X-Tenant

Multipart CSV / XLSX (≤ 10 MB) + a mapping template.

POST /api/ingestion-batches/validate/

Tenant member + X-Tenant

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

{scheme, value}

✱

Producer external id (see Producer identity). On the wire today: a producer_npn string. Omitted when a producer-scoped credential already binds the producer.

external_policy_id

string

✔

The PAS’s stable policy id. Idempotency key.

policy_number

string

—

Human policy number.

product_code

string

—

Maps to reference.Product by code.

jurisdiction_code

string

—

ISO / state code; global-first (never a bare “state” string).

transaction_type

enum

✔

new_business · renewal · endorsement · cancellation — the transaction event.

policy_status

enum

—

in_force · lapsed · cancelled · paid_up · pending · expired. Lifecycle state; defaults to in_force. Scopes status-based commission rates.

written_premium

decimal(14,2)

✔

Native-currency amount.

currency

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.

inception_date

date

—

Original policy issue date, carried unchanged across renewals — the anchor for years-in-force rate bands. New business: equals effective_date when omitted. Renewals/endorsements: send the original inception, or tenure-scoped rates cannot apply.

effective_date

date

✔

Effective date of this transaction (a renewal has its own).

expiration_date

date

—

as_of_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

producer

{scheme, value}

✱

As above.

external_txn_id

string

✔

The PAS’s stable txn id. Idempotency key.

policy_number

string

—

Links the commission to a policy.

kind

enum

✔

base · override · bonus · chargeback.

amount

decimal(14,2)

✔

Signed; a chargeback is a negative earning.

currency

ISO 4217

✔

Native currency; a batch may freely mix currencies.

transaction_date

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 IngestionBatch with policy_count, commission_count and unresolved_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

NPN

US National Producer Number

NIPR / PDB

FCA

UK FCA firm / individual reference

FCA

CODE

Tenant-internal producer_code (fallback where no national registry applies)

—

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’s consume_message normalizes.

  • 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

X-TigerDistribute-Event

The event type.

X-TigerDistribute-Delivery

The event id — subscribers dedupe on this.

X-TigerDistribute-Signature

sha256=<HMAC-SHA256(body, endpoint secret)> — verify before trusting.

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

ProducerOnboardedV1

producer

Producer activated; carries the syncable identity.

ProducerUpdatedV1

producer

Name / identifier / status changed.

ProducerTerminatedV1

producer

Producer ended.

ContractSignedV1

contract

Producer agreement executed.

AppointmentConfirmedV1

appointment

Producer may place in a jurisdiction / company.

AppointmentTerminatedV1

appointment

Appointment ended.

MarketAccessChangedV1

producer

A product-authorization change altered the can-place verdict.

ProducerPayabilityChangedV1

producer

Commission hold / release (affects agency-bill netting).

PayoutInstructedV1

payout

A payment run produced an instruction (see below).

BackgroundCheckCompletedV1

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 via egress_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

isotope

Reference adapter (full)

push · file · pull · push_status · webhook_status

guidewire

Stub

pull · file

sapiens

Stub

file · pull

socotra

Stub

push · bus · webhook_status

duck_creek

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:

  1. Ingest a PolicyFact + CommissionFact batch (any mode) and see counts with zero unexpected unresolved rows.

  2. Resolve identity — its producer identifiers match tigerdistribute producers (or are surfaced as unresolved and bindable).

  3. Receive & verify a signed webhook envelope (signature valid; dedupes on the delivery id).

  4. Reconcile — its commission facts tie to a statement via CommissionReconciliation.

  5. (If gating) consume AppointmentConfirmedV1 / MarketAccessChangedV1 and 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

integrations/models.py — IngestedPolicy, IngestedCommissionTxn, IngestionBatch

Ingestion pipeline

integrations/services/ingestion_service.py

Inbound API

integrations/api/views.py — PASPushView, IngestionBatchViewSet, ProducerIngestView, BordereauxUploadViewSet

Adapter base & registry

integrations/connectors/base.py — PASConnector, ConnectorResult, PushEnvelope

Reference adapter

integrations/connectors/isotope.py

Bus transport

integrations/bus/ — transport ABC + Redis Streams; services/bus_consumer_service.py

Outbound publish

integrations/services/outbox_service.py

Outbound relay & egress

integrations/services/relay_service.py, pas_egress_dispatcher.py, pas_egress_service.py

Payout / settlement

commissions/services/payment_run_service.py

Durable orchestration

temporal/workflows.py, temporal/activities.py (see Operations: background jobs & schedules)