Identity API: Quick Start Guide

This guide takes you from credentials to a working Identity integration — reading your producer roster, resolving contact and financial details, and keeping it current.

The Identity API is the source of record for who a producer is: name, NPN, addresses, phones, bank accounts, and E&O coverage. Where they sit in your distribution network lives in the Contracting API.

Before You Start

  • Sandbox credentials — email support@agentsync.io with the scopes you need:
    • identity.profiles.read — read persons, firms, addresses, phones, bank accounts, E&O policies
    • identity.profiles.writeonly if you create or update records. This one scope covers create, update and delete across every Identity resource; there is no read-plus-create tier. Don't request it for a read-only integration.
  • An access token — see the Authentication guide
  • Python 3.8+ with requests if you're following the Python examples: pip install requests

All Identity requests use an /identity path prefix:

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

Unlike Contracting, you can build sandbox test data through the API with identity.profiles.write — see Step 5.

Step 1: Make Your First Call

List the persons visible to your account.

curl

curl -X GET \
  "https://api.sandbox.agentsync.io/identity/v2/persons?page_size=25" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Python

import requests

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

response = requests.get(
    f"{base_url}/v2/persons", headers=headers, params={"page_size": 25}
)
response.raise_for_status()
data = response.json()
print(f"Fetched {data['page']['size']} person(s)")

Example response

{
  "items": [
    {
      "id": "990e8400-e29b-41d4-a716-446655440004",
      "firstName": "Jordan",
      "lastName": "Rivera",
      "primaryEmail": "jordan.rivera@example.com",
      "npn": "1234567",
      "npnVerified": true,
      "ssnLast4": "6789",
      "createdAt": "2026-03-18T18:19:20Z",
      "updatedAt": "2026-06-02T09:11:00Z"
    }
  ],
  "page": { "size": 1, "nextToken": null }
}

Got a 404? Check the /identity prefix — it's easy to send Identity calls to the bare host. Got a 403? Your token is missing identity.profiles.read.

Two fields worth noting before you model anything:

  • A person's email is primaryEmail, not email. Firms use email. See Email Fields.
  • npnVerified tells you whether the NPN has been checked against NIPR — read it before treating an NPN as a join key.

Step 2: Read Contact Details

Addresses and phones hang off a person or a firm:

curl -X GET \
  "https://api.sandbox.agentsync.io/identity/v2/persons/$PERSON_ID/addresses" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
ResourceTypesKey fields
AddressMAILING, BUSINESS, PHYSICALaddressLine1, addressLine2, city, state, zip, county, country, preferred
PhoneCELL, BUSINESS, RESIDENT, FAXnumber (10 digits), extension, preferred

Both use the same items + page envelope, so your paging code carries over unchanged. Swap persons for firms to read a firm's contact details.

Step 3: Read Financial and Compliance Data

Bank accounts can be read per producer, or account-wide:

# Every bank account visible to you, with owner context on each row
curl -X GET \
  "https://api.sandbox.agentsync.io/identity/v2/bank-accounts" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Each row carries personId or firmId plus npn where known, so you can reconcile your whole population without iterating producer IDs.

E&O policies work the same way via /v2/persons/{personId}/insurance-policies, carrying carrier, policyNumber, totalLimit, effectiveDate, and expiry.

Masked Fields

Sensitive values are masked by default and available in full only from dedicated endpoints, each of which writes to a durable audit log on every access:

FieldDefaultFull value
Person SSNssnLast4GET /v2/persons/{personId}/ssn
Firm FEIN****{last4}GET /v2/firms/{firmId}/fein
Bank account number****{last4}GET /v2/bank-accounts/{bankAccountId}

Call these only when you genuinely need the value — for example, an account number at the point of payment processing. Don't fetch them to populate a cache.

Step 4: Keep Your Roster in Sync

GET /v2/persons supports two filters built for sync pipelines:

curl -X GET \
  "https://api.sandbox.agentsync.io/identity/v2/persons?updated_since=2026-06-01T00:00:00Z" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
  • updated_since — RFC3339 UTC timestamp, inclusive. Store your last run time as a high-water mark and overlap the window slightly.
  • firm_id — restrict to persons associated with a given firm.

GET /v2/firms accepts updated_since on the same terms, so a firm roster syncs the same way.

For near-real-time updates, subscribe to Identity webhook events. The address and phone events carry the producer's full current set, so you can replace stored state without a follow-up read.

Identity events carry PII and financial data — SSN, FEIN, bank account and routing numbers. Log the envelope (id, type, timestamp) rather than the data object, and restrict these subscriptions to systems approved for that class of data.

Step 5: Create a Producer (Optional)

Skip this step if your integration is read-only.

With identity.profiles.write, POST /v2/persons creates a producer identity. Set sendInvite: true to trigger the email invitation in the same call:

curl -X POST \
  "https://api.sandbox.agentsync.io/identity/v2/persons" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Priya",
    "lastName": "Raman",
    "primaryEmail": "priya.raman@example.com",
    "npn": "7654321",
    "sendInvite": true
  }'

The endpoint accepts an idempotency key header, so a retried request won't create a duplicate.

Pagination

Identity list responses are token-paginated. Pass the response's page.nextToken back as the page_token query parameter:

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

page_size defaults to 25 and must be 1–250 — the same limits Contracting enforces, so a paging loop written against one API works unchanged against the other. Out-of-range values are rejected with a 400 rather than clamped.

See Pagination for the full pattern.

Error Handling

Identity returns a machine-readable code on every error — branch on that, not the message:

{
  "message": "Validation failed",
  "details": [
    { "param": "primaryEmail", "message": "must not be blank" }
  ],
  "code": "validation_failed"
}
CodeStatusMeaning
validation_failed400Field-level problems, itemised in details
unauthorized401Token missing, expired, or invalid
unauthorized_scope403Token lacks identity.profiles.read or .write
resource_not_found404No such resource — or it belongs to another account
conflict409Conflicts with an existing record
unprocessable422Well-formed but semantically rejected
rate_limited429Back off — see Rate Limits
internal500Server-side; safe to retry with backoff

Cross-account requests return 404, never 403, so resource existence isn't leaked across account boundaries. A 403 always means a missing scope.

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

Complete Python Reference Implementation

import requests


class IdentityClient:
    """Minimal read client for Identity v2."""

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

    def paginate(self, path, params=None):
        """Yield every item from a v2 token-paginated endpoint."""
        params = dict(params or {})
        params.setdefault("page_size", 250)  # 250 is the maximum the API accepts
        while True:
            response = self.session.get(f"{self.base}{path}", 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 persons(self, updated_since=None, firm_id=None):
        params = {}
        if updated_since:
            params["updated_since"] = updated_since
        if firm_id:
            params["firm_id"] = firm_id
        return self.paginate("/v2/persons", params)

    def addresses(self, person_id):
        return self.paginate(f"/v2/persons/{person_id}/addresses")

    def bank_accounts(self):
        """Account-wide, with personId/firmId owner context on each row."""
        return self.paginate("/v2/bank-accounts")


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

    for person in client.persons():
        addresses = client.addresses(person["id"])
        preferred = next((a for a in addresses if a.get("preferred")), None)
        print(person["npn"], person["primaryEmail"], preferred["city"] if preferred else "—")

Next Steps