Sync with the API
Keep identities current from any identity provider or HR system, such as Workday, Microsoft Entra ID, or Google Workspace, with a scheduled job that pushes changes to the Antenna Enterprise API. Use this when Antenna doesn’t have a native integration for your system, or when your HR system is the source of truth for teams and reporting lines.
You only send each person’s details. Antenna updates automatic groups, such as team and manager groups, from those details for you. To keep the members of your own groups in sync, including when each person joined and left, see Sync Group Memberships.
Prerequisites
- Admin access to your Antenna enterprise
- An enterprise API key from Settings > API Keys
- A way to export people from your system, such as a Workday report or the Microsoft Graph API
- Somewhere to run a scheduled job, such as a cron job, an AWS Lambda function, or a CI pipeline
API Reference
The full reference for every endpoint, field, and response lives in Antenna. Go to Settings > API Keys and choose View API docs, or open https://app.antenna.dev/enterprises/YOUR_ENTERPRISE_SLUG/api_docs. You can find your enterprise slug in the URL of any Antenna page.
Authentication
Send the API key as-is in the Authorization header, without a Bearer prefix:
curl https://app.antenna.dev/api/v1/enterprise/identities \
-H "Authorization: $ANTENNA_API_KEY"Endpoints
| Request | Use |
|---|---|
GET /api/v1/enterprise/identities | List every identity with its details and linked accounts. Results are paginated with page and per_page (default 100); follow the link response header to the next page. |
PATCH /api/v1/enterprise/identities/:id | Update an existing identity. Send only the fields that changed. |
POST /api/v1/enterprise/identities | Create an identity. display_name is required. |
Each identity returned by GET looks like this:
{
"id": 1042,
"display_name": "Jordan Lee",
"email": "jordan.lee@example.com",
"role": "Senior Software Engineer",
"manager": "Priya Shah",
"team": ["Payments", "Platform"],
"department": "Engineering",
"accounts": [
{ "account_type": "GitUser", "provider": "github", "external_id": "5839201" }
]
}Fields
| Field | Type | Notes |
|---|---|---|
display_name | String | Required when creating an identity. |
email | String | Work email. |
role | String | Job title. Used to detect the role category and seniority. |
manager | String | The manager’s display name. See Managers. |
team | Array of strings | Every team the person belongs to. The list you send replaces the current one. |
product_line | Array of strings | Every product line. The list you send replaces the current one. |
business_unit, department, company, location | String | Organizational details. |
employment_type | String | How the person is engaged, such as “Employee” or “Contractor”. |
seniority_override | String | Seniority from your HR system. Send a blank string to detect it from the role. |
role_category_override | String | A role category name. Send a blank string to detect it from the role. |
fully_excluded | Boolean | Excludes the identity from all metrics. |
How the Sync Works
Fetch people from your system
On a schedule, such as nightly, pull every person from your identity provider or HR system with the details you want in Antenna. If your system stores each person’s GitHub or Azure DevOps user ID, include it.
Fetch identities from Antenna
Call GET /api/v1/enterprise/identities and page through every result.
Match each person to an identity
Match on a linked account first: compare the person’s GitHub or Azure DevOps user ID to accounts[].external_id where provider is github or azure_devops. If there’s no account match, match on email, ignoring case.
Map fields to the identity payload
Convert each person’s details to the fields above, for example a Workday supervisory organization to team and a job profile to role. Split multi-value fields into arrays.
Compare and skip unchanged identities
Compare the mapped payload to the identity’s current values. If nothing changed, skip the request.
Update or create
If the identity exists and fields changed, call PATCH /api/v1/enterprise/identities/:id with only the changed fields. If no identity matched, call POST /api/v1/enterprise/identities.
Antenna creates an identity for each Git, project tracker, and AI tool account it sees, so most people already have one. Match carefully before creating identities to avoid duplicates. You can merge duplicates in Settings > Identities.
Here’s an example update:
curl -X PATCH https://app.antenna.dev/api/v1/enterprise/identities/1042 \
-H "Authorization: $ANTENNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"team": ["Payments"], "manager": "Priya Shah", "role": "Staff Software Engineer"}'Sync Group Memberships
Use the groups endpoints for manual groups that your own system tracks, such as a cross-team guild or a program team, when reports need to know who was in the group and when. Automatic groups are built from identity details, so you don’t sync their members.
| Request | Use |
|---|---|
GET /api/v1/enterprise/groups | List every group with its id, display_name, members_count, and synced. Paginated like identities. |
POST /api/v1/enterprise/groups | Create a group. display_name is required. |
PATCH /api/v1/enterprise/groups/:id | Rename a group. |
GET /api/v1/enterprise/groups/:id/memberships | List the group’s members with identity_id, display_name, start_date, and end_date. Paginated. |
PUT /api/v1/enterprise/groups/:id/memberships | Set the group’s members to the list you send. People you leave out are ended, not removed. |
PUT /api/v1/enterprise/groups/:id/memberships/:identity_id | Add one person to the group, or change their start_date and end_date. Dates you leave out are cleared. |
DELETE /api/v1/enterprise/groups/:id/memberships/:identity_id | Remove one person from the group, along with their history in it. |
Find a group’s id with GET /api/v1/enterprise/groups, or in the URL when you open the group in Settings > Groups. Each identity_id is the id from GET /api/v1/enterprise/identities, so run the identity sync first and match people the same way.
curl -X PUT https://app.antenna.dev/api/v1/enterprise/groups/42/memberships \
-H "Authorization: $ANTENNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"memberships": [
{"identity_id": 101, "start_date": "2026-01-01", "end_date": "2026-06-30"},
{"identity_id": 102, "start_date": "2026-07-01"}
]}'- One window per person: each person has one
start_dateand oneend_datein a group, both inclusive. A blankstart_datemeans they were always in the group, and a blankend_datemeans they still are. Reports count a person’s work toward the group only between those dates. - People you leave out are ended, not removed: when you send the whole list, anyone missing from it keeps their history and gets an
end_dateof yesterday, unless they already ended earlier. People whosestart_datehasn’t come yet are removed. To delete a membership added by mistake, useDELETE. - One person at a time: to change a single membership, such as setting an
end_datewhen someone leaves, use the endpoints that end in/:identity_id. - All or nothing: if any entry is invalid, such as an unknown or repeated
identity_idor anend_datebefore thestart_date, nothing changes and the response is422. - Synced groups are read-only: groups with
synced: trueare kept up to date by an integration or automatic rule, soPATCH,PUT, andDELETEreturn422for them.
Things to Know
Managers
manager is matched to another identity by display name, ignoring case. If no identity has that name, Antenna creates one. Send the manager’s name exactly as it appears in Antenna, and sync managers before their reports when you create new identities.
Groups
Manager groups update shortly after a manager changes. Other automatic groups, such as team and department groups, update within two hours.
People Who Leave
Leave departed people in place so their history stays in your reports. fully_excluded removes an identity from metrics entirely, so use it for accounts that should never count, such as test users.
Errors
| Status | Meaning |
|---|---|
401 | The API key is missing, wrong, or revoked. |
404 | No identity or group with that ID in your enterprise. |
422 | The request was rejected. The error field explains why. |
Antenna MCP
The Antenna MCP can look up identities with enterprise_identity_search, but it can’t create or update them. Use the API for syncs.
Example: AWS
A small, serverless setup covers most enterprises:
| Service | Role |
|---|---|
| EventBridge Scheduler | Runs the sync on a schedule, such as daily. |
| Lambda | Runs the sync script. Use an ECS Fargate task instead if a run takes longer than 15 minutes. |
| Secrets Manager | Stores the Antenna API key and the credentials for your HR system or identity provider. |
| CloudWatch and SNS | Logs each run and alerts your team when requests fail. |
The script itself is short:
import os
import requests
BASE = "https://app.antenna.dev/api/v1/enterprise/identities"
HEADERS = {"Authorization": os.environ["ANTENNA_API_KEY"]}
FIELDS = ["display_name", "email", "role", "manager", "team", "department"]
def antenna_identities():
url, params = BASE, {"per_page": 100}
while url:
response = requests.get(url, headers=HEADERS, params=params, timeout=30)
response.raise_for_status()
yield from response.json()
url, params = response.links.get("next", {}).get("url"), None
def sync(people): # people: dicts in the payload shape, plus optional github_id
identities = list(antenna_identities())
by_account = {(a["provider"], a["external_id"]): i for i in identities for a in i["accounts"]}
by_email = {i["email"].lower(): i for i in identities if i["email"]}
for person in people:
identity = by_account.get(("github", person.get("github_id"))) or by_email.get(person["email"].lower())
payload = {field: person[field] for field in FIELDS if field in person}
if identity is None:
requests.post(BASE, headers=HEADERS, json=payload, timeout=30).raise_for_status()
continue
changes = {field: value for field, value in payload.items() if identity.get(field) != value}
if changes:
requests.patch(f"{BASE}/{identity['id']}", headers=HEADERS, json=changes, timeout=30).raise_for_status()