Contracting & Hierarchies API: Quick Start Guide

This guide walks through a working read integration against the Contracting & Hierarchies API v2 — from your first call to a running incremental sync, including reconstructing your distribution hierarchy and reading it as of a past date.

The v2 surface is read-only. Every endpoint is a GET. Contracting records are created and maintained in the AgentSync application; your integration consumes them.

Before You Start

  • Sandbox credentials — email support@agentsync.io to request access with these scopes:
    • contracting.contractassignments.read — contract assignments, assigned carriers/products/commission levels, available uplines, and hierarchies
    • contracting.producers.read — producers
    • contracting.contracts.read — contracts
    • identity.profiles.read — only if you need producer contact details (see Step 5)
  • An access token — see the Authentication guide
  • Your AgentSync organization ID (UUID) — required for the hierarchy endpoint. See Step 4; your AgentSync account team can also provide it.
  • Sandbox test data — your sandbox starts empty, and because v2 is read-only you cannot create Contracting data through the API. Request sandbox application access at the same time as your credentials so someone on your team can build a test hierarchy. See Environments.
  • Python 3.8+ with requests if you're following the Python examples: pip install requests

All requests — contracting and hierarchies alike — use a /contracting path prefix:

EnvironmentBase URL
Sandboxhttps://api.sandbox.agentsync.io/contracting
Productionhttps://api.agentsync.io/contracting

Examples below assume a valid ACCESS_TOKEN.

Hierarchy data is derived from contract assignments. If your account has no carriers, products, or contract assignments yet, the hierarchy call correctly returns an empty page rather than an error.

What You'll Build

  1. Confirm connectivity and read your account configuration
  2. Read your producers
  3. Read contracts and contract assignments
  4. Read your distribution hierarchy, reconstruct the tree, and pull historical snapshots
  5. Resolve producer contact details from the Identity API
  6. Keep it all current with incremental sync and webhooks

Step 1: Make Your First Call

Fetch the carriers assigned to your account. This confirms connectivity, scopes, and base URL in one call.

curl

curl -X GET \
  "https://api.sandbox.agentsync.io/contracting/v2/assigned-carriers" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Python

import requests

base_url = "https://api.sandbox.agentsync.io/contracting"
headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}

response = requests.get(f"{base_url}/v2/assigned-carriers", headers=headers)
response.raise_for_status()
data = response.json()
print(f"Fetched {data['page']['size']} carrier(s)")

Example response

{
  "items": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "Acme Insurance Company",
      "naic": "12345",
      "statusName": "Active",
      "createdAt": "2026-03-18T18:19:20Z",
      "updatedAt": "2026-03-18T18:19:20Z"
    }
  ],
  "page": { "size": 1, "nextToken": null }
}

Every v2 list response uses this envelope: results in items, paging metadata in page.

Got {"items": []}? That's a valid response from an empty sandbox, not a failure. See Before You Start.

Got a 404? Check the /contracting prefix. Got a 403? Your token is missing a scope.

GET /v2/assigned-products and GET /v2/assigned-commission-levels complete the picture of your account configuration.

Step 2: Read Your Producers

curl -X GET \
  "https://api.sandbox.agentsync.io/contracting/v2/producers?page_size=50" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "items": [
    {
      "type": "AGENT",
      "personId": "990e8400-e29b-41d4-a716-446655440004",
      "firmId": null,
      "npn": "1234567",
      "email": "jordan.rivera@example.com",
      "createdAt": "2026-03-18T18:19:20Z",
      "updatedAt": "2026-03-18T18:19:20Z"
    }
  ],
  "page": { "size": 1, "nextToken": null }
}

A producer is one entity discriminated by type. An AGENT carries personId with firmId: null; a FIRM is the reverse. Hold on to these — they're Identity identifiers, used in Step 5.

GET /v2/producers/{producerId} returns the detail view, which additionally embeds the producer's bank accounts with account and routing numbers masked to ****{last4}.

Step 3: Read Contracts and Assignments

Contracts represent the relationship between your organization and a carrier. Contract assignments place a producer against a product under that contract.

curl -X GET \
  "https://api.sandbox.agentsync.io/contracting/v2/contract-assignments?page_size=50" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Assignments inline the names you'd otherwise have to look up — product, commission level, and status all arrive with both id and name, so no second call is needed to render a row.

Related endpoints:

  • GET /v2/contracts, GET /v2/contracts/{id}
  • GET /v2/contract-assignments/{id}
  • GET /v2/contract-assignment-changes — point-in-time amendments, each carrying its own status and snapshot window

Step 4: Read Your Hierarchy

The hierarchy endpoint is scoped by organization ID — the UUID identifying your agency or carrier in AgentSync. If you don't have it, the assigned-carriers response from Step 1 carries your organization context, and your AgentSync account team can confirm it.

curl -X GET \
  "https://api.sandbox.agentsync.io/contracting/v2/organizations/$ORG_ID/hierarchies" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Example response

{
  "items": [
    {
      "contractAssignmentId": "ca-uuid-imo",
      "uplineContractAssignmentId": null,
      "producerName": "Acme IMO",
      "type": "FIRM",
      "personId": null,
      "firmId": "770e8400-e29b-41d4-a716-446655440002",
      "npn": "1234567",
      "productId": "prod-uuid-2222",
      "productName": "Term Life 10-Year",
      "assignmentStatusName": "Approved",
      "commissionLevelName": "L1",
      "commissionLevel": 100.00,
      "stateAbbreviations": ["CA", "TX", "NY"],
      "writingNumber": null,
      "effectiveOn": "2026-01-15",
      "expiresOn": null,
      "active": true
    },
    {
      "contractAssignmentId": "ca-uuid-agency",
      "uplineContractAssignmentId": "ca-uuid-imo",
      "producerName": "Westside Agency",
      "type": "FIRM",
      "personId": null,
      "firmId": "770e8400-e29b-41d4-a716-446655440003",
      "npn": "2345678",
      "productId": "prod-uuid-2222",
      "productName": "Term Life 10-Year",
      "assignmentStatusName": "Approved",
      "commissionLevelName": "L2",
      "commissionLevel": 90.00,
      "stateAbbreviations": ["CA"],
      "writingNumber": null,
      "effectiveOn": "2026-02-01",
      "expiresOn": null,
      "active": true
    },
    {
      "contractAssignmentId": "ca-uuid-producer",
      "uplineContractAssignmentId": "ca-uuid-agency",
      "producerName": "Joe Producer",
      "type": "AGENT",
      "personId": "990e8400-e29b-41d4-a716-446655440004",
      "firmId": null,
      "npn": "15645555",
      "productId": "prod-uuid-2222",
      "productName": "Term Life 10-Year",
      "assignmentStatusName": "Approved",
      "commissionLevelName": "L3",
      "commissionLevel": 80.00,
      "stateAbbreviations": ["CA"],
      "writingNumber": "WN-15645555",
      "effectiveOn": "2026-03-01",
      "expiresOn": null,
      "active": true
    }
  ],
  "page": { "size": 3, "nextToken": null }
}

Reading the flat list:

  • Root nodes: uplineContractAssignmentId == null
  • Direct upline: follow uplineContractAssignmentId to the node whose contractAssignmentId matches
  • Inactive positions: active: false — a node is inactive when its own status is inactive or any node above it is
  • Nodes span all products: the list covers every product in your hierarchy; group or filter by productId for per-product views

Fetch every page before reconstructing — a child can appear on a later page than its parent. For the complete field-by-field schema, see Hierarchy Nodes.

Reconstruct the Tree

Group nodes by uplineContractAssignmentId and attach children to their parents:

nodes = list(client.hierarchy(ORG_ID))          # see the reference implementation below
roots, children = build_tree(nodes)

def walk(node, depth=0):
    print(f"{'  ' * depth}{node['producerName']}{node['commissionLevelName']}")
    for child in children.get(node["contractAssignmentId"], []):
        walk(child, depth + 1)

for root in roots:
    walk(root)

Against the response above, that prints:

Acme IMO — L1
  Westside Agency — L2
    Joe Producer — L3

Get a Point-in-Time Snapshot

Pass as_of (yyyy-MM-dd) to see the hierarchy as it existed on a given date:

curl -X GET \
  "https://api.sandbox.agentsync.io/contracting/v2/organizations/$ORG_ID/hierarchies?as_of=2026-01-15" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

This is the mechanism for commissions reconciliation — query the hierarchy as of the transaction date to confirm who the upline was at that moment.

No as_of → current state. Always supply an explicit as_of when querying for historical purposes. The current state may have changed since the transaction.

Step 5: Resolve Producer Contact Details

Contracting holds placement — where a producer sits. Addresses, phones, bank accounts, and E&O policies live in the Identity API.

The two join directly: the personId and firmId from Step 2 are Identity identifiers. No mapping, no NPN lookup.

# personId came straight from the Contracting producer
curl -X GET \
  "https://api.sandbox.agentsync.io/identity/v2/persons/$PERSON_ID/addresses" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Note the different path prefix (/identity) and the separate scope (identity.profiles.read). See Joining with the Contracting API.

Step 6: Keep Downstream Systems in Sync

Use updated_since to fetch only what changed:

curl -X GET \
  "https://api.sandbox.agentsync.io/contracting/v2/contract-assignments?updated_since=2026-06-01T00:00:00Z" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
  • Takes an RFC3339 UTC timestamp and returns records modified at or after it, inclusive. Store your last sync time as a high-water mark and overlap the window slightly.
  • Available on GET /v2/producers, /v2/contracts, /v2/contract-assignments, /v2/contract-assignment-changes, /v2/assigned-carriers, /v2/assigned-products, and /v2/assigned-commission-levels.
  • Pair it with webhook events for near-real-time updates, and keep updated_since polling as the reconciliation safety net.

The hierarchy endpoint has no updated_since — hierarchy changes arrive as webhook events instead:

EventWhen It Fires
hierarchy.producer.addedA producer is newly placed under an upline
hierarchy.producer.changedA producer's upline and/or commission level changes (changedFields says which)
hierarchy.producer.relationship.terminatedA producer's relationship ends — the assignment enters a terminal status

Event payloads carry the same contractAssignmentId used as the node key in Step 4, so you can update your local tree directly from the event. See Contracting API Webhook Events for payload schemas and the Webhooks Quick Start Guide to register your endpoint.

Detecting firms in events: test firmId for presence, not personId for null. On the REST hierarchy node a FIRM position has personId: null, but in hierarchy.producer.* events personId is always populated. Logic branching on personId == null works against REST and then silently classifies every firm as an individual when pointed at the event stream.

Commissions: Upline Chain at Transaction Time

1. GET /v2/organizations/{orgId}/hierarchies?as_of={transactionDate}
2. Find the producer's node (match on your stored contractAssignmentId)
3. Walk uplineContractAssignmentId links for the full chain
4. Read commissionLevelName / commissionLevel from each node in the chain

Real-Time Hierarchy Sync

1. Initial load: fetch all nodes (Step 4) and store keyed on contractAssignmentId
2. Subscribe to hierarchy.producer.* events
3. On added:      insert the node under uplineContractAssignmentId
   On changed:    update the fields listed in changedFields
   On terminated: mark the node (and its subtree) inactive
4. Periodically reconcile with a full as_of fetch as a safety net

Full Hierarchy Reconciliation

1. GET /v2/organizations/{orgId}/hierarchies?as_of={reconciliationDate}
2. Match each node to your local records on contractAssignmentId
3. In AgentSync but not local  → new positions to add
   Local but not in AgentSync  → terminated or restructured
   In both                     → compare upline, commission level, status

Window Comparison

The API returns point-in-time snapshots — there is no date-range filter.
To find changes within a window:

1. GET .../hierarchies?as_of={windowStart}
2. GET .../hierarchies?as_of={windowEnd}
3. Diff the two sets on contractAssignmentId

Pagination

List responses are token-paginated. When page.nextToken is non-null, pass it back as the page_token query parameter:

curl -H "Authorization: Bearer $ACCESS_TOKEN" \
  "https://api.sandbox.agentsync.io/contracting/v2/producers?page_token=NEXT_TOKEN"

The parameter is page_token, not nextToken. The response field and the query parameter are spelled differently, and unrecognized parameters are ignored rather than rejected — so the wrong name silently returns the first page again instead of an error.

page_size defaults to 25 and must be 1–250; anything outside that range is rejected with a 400 rather than clamped. See Pagination for the full pattern.

Error Handling

StatusLikely causeFix
400Invalid page_size, malformed page_token, or bad as_of/updated_since formatCheck the parameter format; page_size must be 1–250
401Token expiredRe-authenticate and retry once
403Token is missing a required scopeCheck the scope for that endpoint in Authentication
404Wrong path prefix, an organization ID not associated with your credentials, or a resource belonging to another accountConfirm the /contracting prefix and your organization ID; cross-account resources return 404, never 403
429Rate limit exceededBack off exponentially — see Rate Limits

v2 errors return a compact object:

{
  "message": "page_size must be between 1 and 250 but was 5000",
  "details": []
}

Capture the DD-Trace-ID response header on failures and include it in support tickets — see Traceability.

Token Management

Tokens are valid for 60 minutes — reuse them across requests rather than fetching one per call, which is the most common way integrations hit the token endpoint's rate limit. See Authentication: Token Expiration & Reuse for a reusable client.

Complete Python Reference Implementation

A read integration that pages through producers and assignments, resolves contact details from Identity, and supports incremental runs.

import requests


class ContractingClient:
    """Minimal read client for Contracting v2 + Identity v2."""

    def __init__(self, access_token, base="https://api.sandbox.agentsync.io"):
        self.contracting = f"{base}/contracting"
        self.identity = f"{base}/identity"
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {access_token}"

    def paginate(self, url, params=None):
        """Yield every item from a v2 token-paginated endpoint."""
        params = dict(params or {})
        while True:
            response = self.session.get(url, params=params)
            response.raise_for_status()
            data = response.json()

            yield from data["items"]

            token = data["page"]["nextToken"]
            if token is None:
                return
            params["page_token"] = token

    def producers(self, updated_since=None):
        params = {"page_size": 250}
        if updated_since:
            params["updated_since"] = updated_since
        return self.paginate(f"{self.contracting}/v2/producers", params)

    def hierarchy(self, organization_id, as_of=None):
        params = {"as_of": as_of} if as_of else {}
        return self.paginate(
            f"{self.contracting}/v2/organizations/{organization_id}/hierarchies", params
        )

    def addresses_for(self, producer):
        """Resolve a Contracting producer's addresses from the Identity API.

        personId/firmId on a producer are Identity identifiers — no mapping needed.
        """
        if producer["type"] == "FIRM":
            path = f"{self.identity}/v2/firms/{producer['firmId']}/addresses"
        else:
            path = f"{self.identity}/v2/persons/{producer['personId']}/addresses"
        return list(self.paginate(path))


def build_tree(nodes):
    """Rebuild the hierarchy from the flat node list."""
    children = {}
    roots = []
    for node in nodes:
        upline = node["uplineContractAssignmentId"]
        if upline is None:
            roots.append(node)
        else:
            children.setdefault(upline, []).append(node)
    return roots, children


if __name__ == "__main__":
    client = ContractingClient(access_token="YOUR_ACCESS_TOKEN")

    # Full producer roster, with contact details resolved from Identity.
    for producer in client.producers():
        addresses = client.addresses_for(producer)
        print(producer["npn"], producer["type"], len(addresses), "address(es)")

    # Hierarchy, fetched completely before rebuilding the tree.
    nodes = list(client.hierarchy("YOUR_ORG_ID"))
    roots, children = build_tree(nodes)
    print(f"{len(nodes)} node(s), {len(roots)} root(s)")

Next Steps