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 policiesidentity.profiles.write— only 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
requestsif you're following the Python examples:pip install requests
All Identity requests use an /identity path prefix:
| Environment | Base URL |
|---|---|
| Sandbox | https://api.sandbox.agentsync.io/identity |
| Production | https://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/identityprefix — it's easy to send Identity calls to the bare host. Got a403? Your token is missingidentity.profiles.read.
Two fields worth noting before you model anything:
- A person's email is
primaryEmail, notemail. Firms useemail. See Email Fields. npnVerifiedtells 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"
| Resource | Types | Key fields |
|---|---|---|
| Address | MAILING, BUSINESS, PHYSICAL | addressLine1, addressLine2, city, state, zip, county, country, preferred |
| Phone | CELL, BUSINESS, RESIDENT, FAX | number (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:
| Field | Default | Full value |
|---|---|---|
| Person SSN | ssnLast4 | GET /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 thedataobject, 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"
}
| Code | Status | Meaning |
|---|---|---|
validation_failed | 400 | Field-level problems, itemised in details |
unauthorized | 401 | Token missing, expired, or invalid |
unauthorized_scope | 403 | Token lacks identity.profiles.read or .write |
resource_not_found | 404 | No such resource — or it belongs to another account |
conflict | 409 | Conflicts with an existing record |
unprocessable | 422 | Well-formed but semantically rejected |
rate_limited | 429 | Back off — see Rate Limits |
internal | 500 | Server-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
- Identity API Overview — full resource model and entity relationships
- Joining with the Contracting API — resolve Contracting
personId/firmIdagainst Identity - Identity API Webhook Events — the full event catalog with payload schemas
- Webhooks Quick Start — register an endpoint and validate delivery
- Pagination · Rate Limits · Traceability