Sync people from your own API
Point Polzy at an HTTPS endpoint that lists your people and it reads it on a schedule — and your system can push signed changes the moment they happen.
On this page
The Custom API source connects Polzy to a system of your own — an internal HR tool, a CRM, a database behind an API. It works two ways, and you can use both:
- Pull. Polzy reads an HTTPS endpoint that returns your people as JSON, on a schedule you choose.
- Push. Your system sends one person to Polzy the moment they change, signed with a secret you share, and Polzy applies it straight away.
Either way, the same field mapping turns your records into Polzy's people.
People sync is on Team and up, and only workspace admins can connect a source. Push also needs the Integrations page, which comes with Business, to copy the address your system sends to.
Before you start
You need:
- To be an admin of the Polzy workspace, on Team or higher, with People sync switched on under Settings → General → What this workspace does.
- An endpoint on the public internet, served over HTTPS, that returns a JSON list of people — and a developer who can change it. Polzy does not call private or internal addresses.
- A unique
idon every person. Polzy uses it to recognise people from one sync to the next. - The fields you want to group results by already added in Polzy.
A workspace syncs from one source at a time. If another source is connected, disconnect it first. If your organization provisions people through SCIM, a people source cannot be connected at all — see SCIM.
What your endpoint returns
A GET request to your endpoint returns the people, as a list at the top of the response or at a path inside it:
{
"data": {
"users": [
{
"id": "4821",
"email": "anouk@acme.com",
"first_name": "Anouk",
"last_name": "Jansen",
"phone": "+31612345678",
"is_active": true,
"department": "Sales",
"manager_email": "rahul@acme.com"
}
]
},
"meta": { "next_cursor": "eyJwYWdlIjoyfQ" }
}
Here Data Path (JSON) is data.users; with a plain list at the top, leave it empty. Paths can reach into lists too, such as results[0].people.
Each request has 30 seconds to answer. If your endpoint pages, tell Polzy how:
- By a link or cursor. Put the field that holds it in Next-page field, such as
meta.next_cursor. A full address is called as it is; a cursor is added to your endpoint's address as?cursor=…, or under the name in Page parameter. Polzy stops when the field is empty. - By page number. Leave Next-page field empty and put the parameter's name in Page parameter, such as
page. Polzy asks for page 2, 3 and so on, and stops at the first empty page.
Polzy reads at most 500 pages in one sync.
Connect your API in Polzy
- Open Contacts → Sync & fields.
- Under Available sources, find Custom API and choose Connect. A panel opens with three steps: Connection, Sync settings and Field mapping.
- Enter your API Endpoint URL, and the Data Path (JSON) and paging fields if you need them.
- Choose the Authentication Type and fill in what it asks for: a Bearer Token; an API Key Header Name and API Key Value; a Username and Password for Basic Auth; or nothing for None. Add any other header your endpoint needs under Custom Headers.
- For push, enter a long random string as the Webhook signing secret, and give the same string to your developer. Otherwise leave it empty.
- Choose Test connection. Successfully connected to API. Found N users. means Polzy could read your endpoint.
- Choose Next, check the sync settings, choose Next again, map the fields (below), then choose Connect.
Tokens, passwords, the signing secret and every custom header's value are stored encrypted and only used to call your endpoint.
Sync settings
| Setting | What it does | Default |
|---|---|---|
| Sync Frequency | How often Polzy reads your endpoint: every 30 minutes, every hour, every 4 hours or daily. | Every hour |
| Disable Removed Users | Someone Polzy brought in from your API who no longer appears in it is deactivated at the next sync. | On |
| Send Invitation Emails | New members with an email address get an invitation to sign in to Polzy. Contacts never do. | On |
| Default Role | The role new members get: Viewer, Editor or Admin. It applies only to people the sync creates. | Viewer |
| Save Raw Data | Keeps a copy of each record as your API sent it beside the person in Polzy. Leave it off unless you are troubleshooting a mapping: it stores more personal data than you need. | Off |
Whether your people arrive as members or contacts follows your workspace's audience: members in a workspace for employees, who can sign in and use a seat; contacts in one for customers, product users or event guests, who only answer.
Map the fields
The default mapping reads email, first_name, last_name, phone and is_active. Everything else you map yourself: switch on Custom field mapping and type, for each field, the path in your records that fills it — department, manager_email, plan, profile.country. The list suggests common names, and the paths use the same notation as Data Path. Each row also has an If empty, use value, written when a record has nothing for that field; a value your API does send is never overwritten.
A few things to know:
- Real examples. Once connected, choose Settings on Sync & fields and open Field mapping again: the suggestions then include every path in five real records, each with an Example.
- Active status. A value of false, 0, no, inactive, terminated, leave, disabled, suspended or archived deactivates the person; anything else, or nothing, keeps them active.
- Managers. Map Manager to a field holding the manager's email address, employee ID or
idin your API. The link is made when the manager is in Polzy too. Managers need Reporting lines switched on in the workspace. - Only your workspace's fields are filled. Add a field with Add field on Sync & fields before you map it. To compare results by it, make it a List field and switch on Groups.
Preview and run the first sync
- On Sync & fields, choose Preview a sync under Sync history. Polzy works out what a sync would add, update and deactivate, without changing anything, and opens the log where the result appears as Preview.
- Check the preview, fix the mapping under Settings if needed, then choose Sync Now.
People are matched by their id, and then by email address, so someone you had already added by hand is updated rather than added twice.
Push changes as they happen
Push sends one person at a time to Polzy, signed, whenever they change. Scheduled syncs keep running beside it and catch anything a push missed — so Polzy still needs the endpoint above, even if it returns an empty list.
- In Polzy, open Integrations, then Custom API, and copy the address under Receiving. It is made for your connection and ends in
/api. - Make sure the same secret is saved in Webhook signing secret under Settings and in your system.
- From your system, send a
POSTto that address with the person as JSON and three headers:
| Header | Value |
|---|---|
X-Polzy-Timestamp | The current Unix time, in seconds. |
X-Polzy-Signature | The hex HMAC-SHA256 of the timestamp, a dot and the exact body: <timestamp>.<body>, made with the secret. |
X-Polzy-Event-Id | An ID for this change, the same if you send it again. |
The body is the person, with the same field names your endpoint returns, so the same mapping applies. It may also be wrapped as { "user": { … } } or { "employee": { … } }. It must carry the id and the fields that changed — a body with an id alone is accepted but changes nothing. To deactivate someone, send them with "is_active": false, or whatever your mapping reads for Active status.
In Node.js:
import crypto from 'node:crypto';
const body = JSON.stringify({
id: '4821',
email: 'anouk@acme.com',
first_name: 'Anouk',
last_name: 'Jansen',
department: 'Operations',
is_active: true,
});
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac('sha256', process.env.POLZY_SIGNING_SECRET)
.update(`${timestamp}.${body}`)
.digest('hex');
await fetch(process.env.POLZY_RECEIVING_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Polzy-Timestamp': timestamp,
'X-Polzy-Signature': signature,
'X-Polzy-Event-Id': 'person-4821-2026-10-31T09:12:00Z',
},
body,
});
In Python:
import hashlib, hmac, json, os, time, requests
body = json.dumps({"id": "4821", "email": "anouk@acme.com", "department": "Operations", "is_active": True})
timestamp = str(int(time.time()))
signature = hmac.new(
os.environ["POLZY_SIGNING_SECRET"].encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256,
).hexdigest()
requests.post(
os.environ["POLZY_RECEIVING_URL"],
data=body,
headers={
"Content-Type": "application/json",
"X-Polzy-Timestamp": timestamp,
"X-Polzy-Signature": signature,
"X-Polzy-Event-Id": "person-4821-2026-10-31T09:12:00Z",
},
)
Sign the exact bytes you send: serialise the body once and send that same string. What Polzy answers:
| Answer | Meaning |
|---|---|
201 with { "received": true, "events": 1 } | Accepted. The change appears in Full log as a run marked Webhook. |
201 with { "received": true, "duplicate": true } | This X-Polzy-Event-Id was handled before; nothing was done twice. |
401 with { "error": "Invalid webhook signature", "statusCode": 401 } | No secret is saved in Polzy, the signature does not match, or the timestamp is more than five minutes from now. |
429 | More than 1,200 calls a minute from one address. Wait for the Retry-After time and send again. |
If your workspace is paused for being over its plan, calls are acknowledged and nothing is applied.
Calls signed without a timestamp
The first version of push signed the body alone, without X-Polzy-Timestamp. Those calls are still accepted for now, and while your system sends them Settings → API → Webhooks warns with the date from which they will be refused. From that date, Polzy answers them with 401 and Calls without X-Polzy-Timestamp are refused since that date. Switch to the signature above before then — see Webhooks.
Troubleshooting
| What you see | Why | What to do |
|---|---|---|
| A valid https API URL is required | The address does not start with https://. | Serve your endpoint over HTTPS. |
| API response is not an array. Got: object. Check your Data Path configuration. | The list is inside the response, or Data Path points to the wrong place. | Set Data Path (JSON) to the path of the list, such as data.users. |
| Authentication failed. Check your credentials. | Your endpoint answered 401. | Check the authentication type and the token, key or password. |
| Access forbidden. Check API permissions. | Your endpoint answered 403. | Give the credentials Polzy uses access to the list. |
| API endpoint not found. Check the URL. | Your endpoint answered 404. | Check the address. |
| Connection refused. Check if the API URL is correct and accessible. | Nothing answered at that address. | Make sure the endpoint is reachable from the internet. |
| Everyone appears twice, or nobody is deactivated | Your records have no top-level id, or it changes between syncs. | Give every record a stable id. |
| Pushes are refused with Invalid webhook signature | The secret differs, the body was changed after signing, or the server's clock is off. | Compare the secrets, sign the exact string you send, and keep your clock in sync. |
| N people were not imported: the plan has no seats left for them. | Members use seats, and the plan is full. | Free seats or change plan, then choose Sync Now. |
Good to know
- Limits. A sync that would go over your seats or contacts imports the people who fit, stops there and tells admins — see Usage and limits.
- One person per push. Send a separate call for each person who changed.
- Disconnect or remove. Under Settings, Disconnect stops syncing and keeps the settings and history, so you can reconnect later; Remove deletes the connection. Either way, the people already synced stay in Polzy and are no longer updated, and pushes are refused.
- The rest of the API. To read results or create actions from your system, use an API key and the API reference.
Related articles
Signed-in members can ask Polzy or message the team from Support. How fast we reply depends on your plan.