Contracting API: Best Practices
Use uplineFirmName and uplinePersonName, Not uplineName
The uplineName field is deprecated and will be removed in a future release. Use the appropriate replacement:
uplineFirmName— when the upline entity is an organization or firmuplinePersonName— when the upline entity is an individual producer
Update any existing integrations that write uplineName to use the correct field.
Commission Level Type Casing
The type field on commission levels is case-sensitive and must be one of exactly:
| Value | Use When |
|---|---|
"Percent" | Commission expressed as a percentage (e.g., 55%) |
"Dollar" | Commission expressed as a flat dollar amount |
"Number" | Commission expressed as a numeric multiplier |
Sending "PERCENT", "percent", or "NUMBER" will return a 400 Bad Request. Validate this in your code before sending.
Use Bulk Endpoints for High-Volume Operations
When creating or updating many records at once, use the bulk endpoints to reduce round trips and stay within rate limits:
| Bulk Endpoint | Use Case | Body shape |
|---|---|---|
POST /v1/customers/{productId}/commissionLevels/bulk | Create many commission levels for a product at once | JSON array [...] |
PUT /v1/customers/commissionLevels/bulk | Update multiple commission levels in one call | JSON object (see note below) |
PUT /v1/customers/contractAssignments/bulk | Update multiple contract assignments in one call | JSON object (see note below) |
Note: The
PUTbulk endpoints expect a JSON object as the request body, not an array. This differs fromPOST /bulk, which takes an array directly. Sending an array to aPUTbulk endpoint returns a400 Bad Request. Refer to the API reference for the exact object shape.
Example: Bulk-create commission levels
curl -X POST \
"https://api.sandbox.agentsync.io/contracting/v1/customers/PRODUCT_ID/commissionLevels/bulk" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '[
{ "name": "Level 1", "type": "Percent", "firstYearValue": 55.00, "level": 1 },
{ "name": "Level 2", "type": "Percent", "firstYearValue": 60.00, "level": 2 },
{ "name": "Level 3", "type": "Percent", "firstYearValue": 65.00, "level": 3 }
]'
HAL+JSON Response Format
All Contracting API collection responses follow the HAL+JSON standard:
{
"_embedded": {
"carriers": [ ... ]
},
"_links": {
"self": { "href": "/v1/customers/carriers?page=0&size=250" },
"next": { "href": "/v1/customers/carriers?page=1&size=250" },
"last": { "href": "/v1/customers/carriers?page=7&size=250" }
},
"page": {
"size": 250,
"totalElements": 150,
"totalPages": 1,
"number": 0
}
}
| Key | Description |
|---|---|
_embedded | Result array keyed by resource type (e.g., carriers, products, commissionLevels) |
_links | Navigation links — self always present; next, prev, first, last appear when applicable |
page.totalElements | Total records across all pages |
page.totalPages | Total number of pages |
page.number | Current page index (0-based) |
Individual resource responses include _links.self. Note that for carrier resources, this currently points to /v1/organizations/{id} rather than /v1/customers/carriers/{id} — this path returns 403 Forbidden if followed directly. Use the id field to construct resource URLs yourself.
Paginate All List Responses
Always paginate — don't assume a single response contains all records. See Pagination for response structure, query parameters, and a reusable Python paginator.
Sync Contract Assignment Changes, Don't Requery Everything
Instead of re-fetching all contract assignments on a schedule, poll the contractAssignmentChanges endpoint to get only what changed:
GET /contracting/v1/customers/contractAssignmentChanges?updatedSince=2026-03-01
This is significantly more efficient for keeping a downstream system in sync with the current state of assignments. Use a stored lastSyncDate to make the updatedSince value dynamic.
Validate Payload Fields Before Sending
The API returns 400 Bad Request for constraint violations. Validate client-side to avoid unnecessary round trips:
| Field | Constraint |
|---|---|
naic | Max 6 characters |
tin | Max 9 characters |
npn | Max 15 characters |
commissionLevel.firstYearValue | Max 99999.99 |
commissionLevel.level | Max 99999.99 |
commissionLevel.type | Must be "Percent", "Dollar", or "Number" |
Lookup Reference Data at Startup, Not Per Request
The lookup endpoints (/v1/linesOfBusiness, /v1/annualizations, /v1/assignmentStatuses, /v1/contractSubmissionMethods, /v1/administrativeDivisions) return reference data that changes infrequently. Fetch and cache this data at startup rather than calling these endpoints on every create/update operation.
class ContractingClient:
def __init__(self, client_id, client_secret, base_url, token_url):
# For a complete ContractingClient implementation including auth and all core operations,
# see the Complete Python Reference Implementation in the Quick Start Guide:
# /contracting-api-quick-start-guide#complete-python-reference-implementation
self._lookup_cache = {}
def get_token(self) -> str:
"""Return a valid token, refreshing if expired.
See /api-authentication#token-expiration--reuse for a TokenClient implementation.
"""
raise NotImplementedError("Implement using TokenClient from /api-authentication")
def get_lookup(self, resource: str) -> list:
if resource not in self._lookup_cache:
response = requests.get(
f"{self.base_url}/v1/{resource}",
headers={"Authorization": f"Bearer {self.get_token()}"}
)
response.raise_for_status()
data = response.json()
# Extract items from _embedded (key name matches resource)
items = list(data.get("_embedded", {}).values())
self._lookup_cache[resource] = items[0] if items else []
return self._lookup_cache[resource]
Reliability
- Implement exponential backoff for
429 Too Many Requestsand5xxerrors - Idempotency:
POSTendpoints are not guaranteed idempotent — check for existing records before creating to avoid duplicates - Handle partial bulk failures: Bulk endpoints may return partial success; parse the response to identify which items failed
Performance
- Use the
sizequery parameter to fetch larger pages (up to the API maximum) when you expect many records - Filter on the server side using query parameters rather than fetching everything and filtering client-side
- Run bulk operations during off-peak hours when possible for best throughput
Integration Patterns
Initial Data Load
When setting up a new integration:
- Fetch all reference data (lines of business, annualizations, etc.) and cache locally
- Create all carriers
- For each carrier, create products
- For each product, bulk-create commission levels
- Bulk-create contract assignments for your producer population
Ongoing Sync
For keeping a downstream system in sync:
- Poll
contractAssignmentChangeswithupdatedSinceset to your last sync timestamp - For each changed assignment, fetch the current state with
GET /v1/customers/contractAssignments/{id} - Update your local record and advance your
lastSyncDate
Producer Onboarding
When a new producer joins your network:
- Verify the producer exists in your population via
GET /v1/customers/producers?npn={npn} - Create contract assignments for the relevant products
POST /v1/invitationto trigger the AgentSync onboarding email
Support
Questions? Contact support@agentsync.io