✨ Software.com is now Antenna. Read more →
Skip to Content
People & IdentitySync with the API

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

RequestUse
GET /api/v1/enterprise/identitiesList 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/:idUpdate an existing identity. Send only the fields that changed.
POST /api/v1/enterprise/identitiesCreate 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

FieldTypeNotes
display_nameStringRequired when creating an identity.
emailStringWork email.
roleStringJob title. Used to detect the role category and seniority.
managerStringThe manager’s display name. See Managers.
teamArray of stringsEvery team the person belongs to. The list you send replaces the current one.
product_lineArray of stringsEvery product line. The list you send replaces the current one.
business_unit, department, company, locationStringOrganizational details.
employment_typeStringHow the person is engaged, such as “Employee” or “Contractor”.
seniority_overrideStringSeniority from your HR system. Send a blank string to detect it from the role.
role_category_overrideStringA role category name. Send a blank string to detect it from the role.
fully_excludedBooleanExcludes the identity from all metrics.

How the Sync Works

YOUR SYSTEMSource of truthYOUR SYNC JOBRuns on a scheduleANTENNAEnterprise APIIdentity provideror HR systemWorkday · Entra ID · HRISFIELDSteam · manager · roledepartment · locationGitHub or ADO user IDs1Pull people2Fetch identitiesEvery page, with accounts3Match each personAccount ID, then email4Compare fieldsNew personcreateChangedupdateUnchangedskip, no requestGET /identitiesPaginatedPOST /identitiesPATCH /identities/:idAutomatic groupsUpdated from details

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.

RequestUse
GET /api/v1/enterprise/groupsList every group with its id, display_name, members_count, and synced. Paginated like identities.
POST /api/v1/enterprise/groupsCreate a group. display_name is required.
PATCH /api/v1/enterprise/groups/:idRename a group.
GET /api/v1/enterprise/groups/:id/membershipsList the group’s members with identity_id, display_name, start_date, and end_date. Paginated.
PUT /api/v1/enterprise/groups/:id/membershipsSet 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_idAdd 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_idRemove 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_date and one end_date in a group, both inclusive. A blank start_date means they were always in the group, and a blank end_date means 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_date of yesterday, unless they already ended earlier. People whose start_date hasn’t come yet are removed. To delete a membership added by mistake, use DELETE.
  • One person at a time: to change a single membership, such as setting an end_date when 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_id or an end_date before the start_date, nothing changes and the response is 422.
  • Synced groups are read-only: groups with synced: true are kept up to date by an integration or automatic rule, so PATCH, PUT, and DELETE return 422 for 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

StatusMeaning
401The API key is missing, wrong, or revoked.
404No identity or group with that ID in your enterprise.
422The 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:

ServiceRole
EventBridge SchedulerRuns the sync on a schedule, such as daily.
LambdaRuns the sync script. Use an ECS Fargate task instead if a run takes longer than 15 minutes.
Secrets ManagerStores the Antenna API key and the credentials for your HR system or identity provider.
CloudWatch and SNSLogs 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()
Last updated on