Happily.ai Get an API key

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.

Important

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_exists for 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.

Next steps