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 hierarchiescontracting.producers.read— producerscontracting.contracts.read— contractsidentity.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
requestsif you're following the Python examples:pip install requests
All requests — contracting and hierarchies alike — use a /contracting path prefix:
| Environment | Base URL |
|---|---|
| Sandbox | https://api.sandbox.agentsync.io/contracting |
| Production | https://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
- Confirm connectivity and read your account configuration
- Read your producers
- Read contracts and contract assignments
- Read your distribution hierarchy, reconstruct the tree, and pull historical snapshots
- Resolve producer contact details from the Identity API
- 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/contractingprefix. Got a403? 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
uplineContractAssignmentIdto the node whosecontractAssignmentIdmatches - 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
productIdfor 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 explicitas_ofwhen 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_sincepolling as the reconciliation safety net.
The hierarchy endpoint has no updated_since — hierarchy changes arrive as webhook events instead:
| Event | When It Fires |
|---|---|
hierarchy.producer.added | A producer is newly placed under an upline |
hierarchy.producer.changed | A producer's upline and/or commission level changes (changedFields says which) |
hierarchy.producer.relationship.terminated | A 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
firmIdfor presence, notpersonIdfor null. On the REST hierarchy node aFIRMposition haspersonId: null, but inhierarchy.producer.*eventspersonIdis always populated. Logic branching onpersonId == nullworks against REST and then silently classifies every firm as an individual when pointed at the event stream.
Recommended Workflows
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, notnextToken. 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
| Status | Likely cause | Fix |
|---|---|---|
400 | Invalid page_size, malformed page_token, or bad as_of/updated_since format | Check the parameter format; page_size must be 1–250 |
401 | Token expired | Re-authenticate and retry once |
403 | Token is missing a required scope | Check the scope for that endpoint in Authentication |
404 | Wrong path prefix, an organization ID not associated with your credentials, or a resource belonging to another account | Confirm the /contracting prefix and your organization ID; cross-account resources return 404, never 403 |
429 | Rate limit exceeded | Back 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
- Contracting & Hierarchies API Overview — the full endpoint listing and hierarchy node schema
- Contracting API Best Practices — sync patterns and lookup caching
- Contracting API Webhook Events — react to assignment and hierarchy changes
- Identity API Quick Start — producer contact, bank account, and E&O data
- Pagination · Rate Limits · Traceability