Sync members from your HRIS
Keep your Happily directory in step with your source of truth. The member import endpoint bulk-creates and updates members in one call, with built-in validation and duplicate handling.
Endpoint
POST /api/v1/members
Submit up to 1,000 members per request. Imports are queued for asynchronous processing and return an import_id you can log for traceability.
Request body
{
"members": [
{
"email": "jordan@acme.com",
"first_name": "Jordan",
"last_name": "Diaz",
"team_name": "Design",
"manager_email": "lee@acme.com",
"language": "en",
"country": "US",
"timezone": -7
}
],
"options": {
"skip_duplicates": true,
"update_if_exists": true
}
}
Member fields
| Field | Required | Rules |
|---|---|---|
email |
Yes | Valid email, max 255 chars. |
first_name |
Yes | Non-empty, max 100 chars, no leading/trailing spaces. |
last_name |
Yes | Non-empty, max 100 chars. |
team_name |
No | Max 100 chars. The team is auto-created if it doesn't exist. |
manager_email |
No | Valid email. Must already exist or be present in the same batch. |
language |
No | Lowercase 2–5 letter code, e.g. en, th. Defaults to your company default. |
country |
No | Two-letter uppercase code, e.g. US, TH. Defaults to your company default. |
timezone |
No | Integer UTC offset between -12 and 14. |
Options
| Option | Default | Behavior |
|---|---|---|
skip_duplicates |
false |
Silently skip members whose email already exists. |
update_if_exists |
false |
Update existing members instead of failing. |
If neither option is set, a duplicate email fails validation.
Validation is all-or-nothing for new members. If any member is invalid (and not skipped), the whole request returns VALIDATION_ERROR with a per-row errors array and nothing is imported. Validate your data, or set skip_duplicates / update_if_exists, before going to production.
Make the call
curl -X POST "https://api.happily.ai/prod/api/v1/members" \
-H "x-api-key: $HAPPILY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"members": [
{ "email": "jordan@acme.com", "first_name": "Jordan", "last_name": "Diaz", "team_name": "Design", "manager_email": "lee@acme.com" },
{ "email": "sam@acme.com", "first_name": "Sam", "last_name": "Rivera", "team_name": "Design", "manager_email": "lee@acme.com" }
],
"options": { "skip_duplicates": true, "update_if_exists": true }
}'
const payload = {
members: [
{ email: "jordan@acme.com", first_name: "Jordan", last_name: "Diaz", team_name: "Design", manager_email: "lee@acme.com" },
{ email: "sam@acme.com", first_name: "Sam", last_name: "Rivera", team_name: "Design", manager_email: "lee@acme.com" },
],
options: { skip_duplicates: true, update_if_exists: true },
};
const res = await fetch("https://api.happily.ai/prod/api/v1/members", {
method: "POST",
headers: {
"x-api-key": process.env.HAPPILY_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const result = await res.json();
console.log(result.summary);
import os, requests
payload = {
"members": [
{"email": "jordan@acme.com", "first_name": "Jordan", "last_name": "Diaz", "team_name": "Design", "manager_email": "lee@acme.com"},
{"email": "sam@acme.com", "first_name": "Sam", "last_name": "Rivera", "team_name": "Design", "manager_email": "lee@acme.com"},
],
"options": {"skip_duplicates": True, "update_if_exists": True},
}
res = requests.post(
"https://api.happily.ai/prod/api/v1/members",
headers={"x-api-key": os.environ["HAPPILY_API_KEY"]},
json=payload,
)
print(res.json()["summary"])
Response
{
"status": "success",
"import_id": "imp_1717512730123",
"summary": { "total_submitted": 2, "successfully_imported": 2, "skipped": 0, "failed": 0 },
"details": {
"imported": [
{ "email": "jordan@acme.com", "team_name": "Design" },
{ "email": "sam@acme.com", "team_name": "Design" }
],
"skipped": [],
"failed": []
},
"rate_limit": { "remaining": 9, "reset_at": "2026-06-04T14:33:00+07:00" }
}
Read summary for totals, details for the per-member breakdown, and rate_limit to pace your next call.
Production tips
- Batch fully. Pack up to 1,000 members per request to stay within the 10 requests/minute limit.
- Order managers first. If a manager is being created in the same batch, that's fine. Otherwise the manager must already exist.
- Use
update_if_existsfor ongoing syncs. Run a nightly job that submits your full roster; existing members are updated, new ones are created. - Log the
import_id. It ties your sync job to the queued import for support and debugging.
Get an API key