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 firm
  • uplinePersonName — 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:

ValueUse 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 EndpointUse CaseBody shape
POST /v1/customers/{productId}/commissionLevels/bulkCreate many commission levels for a product at onceJSON array [...]
PUT /v1/customers/commissionLevels/bulkUpdate multiple commission levels in one callJSON object (see note below)
PUT /v1/customers/contractAssignments/bulkUpdate multiple contract assignments in one callJSON object (see note below)

Note: The PUT bulk endpoints expect a JSON object as the request body, not an array. This differs from POST /bulk, which takes an array directly. Sending an array to a PUT bulk endpoint returns a 400 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
  }
}
KeyDescription
_embeddedResult array keyed by resource type (e.g., carriers, products, commissionLevels)
_linksNavigation links — self always present; next, prev, first, last appear when applicable
page.totalElementsTotal records across all pages
page.totalPagesTotal number of pages
page.numberCurrent 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:

FieldConstraint
naicMax 6 characters
tinMax 9 characters
npnMax 15 characters
commissionLevel.firstYearValueMax 99999.99
commissionLevel.levelMax 99999.99
commissionLevel.typeMust 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 Requests and 5xx errors
  • Idempotency: POST endpoints 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 size query 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:

  1. Fetch all reference data (lines of business, annualizations, etc.) and cache locally
  2. Create all carriers
  3. For each carrier, create products
  4. For each product, bulk-create commission levels
  5. Bulk-create contract assignments for your producer population

Ongoing Sync

For keeping a downstream system in sync:

  1. Poll contractAssignmentChanges with updatedSince set to your last sync timestamp
  2. For each changed assignment, fetch the current state with GET /v1/customers/contractAssignments/{id}
  3. Update your local record and advance your lastSyncDate

Producer Onboarding

When a new producer joins your network:

  1. Verify the producer exists in your population via GET /v1/customers/producers?npn={npn}
  2. Create contract assignments for the relevant products
  3. POST /v1/invitation to trigger the AgentSync onboarding email

Support

Questions? Contact support@agentsync.io