User Provisioning
POST /api/v1/users/invite adds a person to an Align organisation, or finds them if they are already there. Integrations use it to provision team members during onboarding. POST /api/v1/users/bulk-invite does the same for up to 50 people in one request.
Requirements
- An organisation-scoped key with
write:user. Project-scoped keys get403 scope_violation. - The organisation must have the Illumera add-on. Without it the endpoint returns
403with{ "error": "addon_required", "addon": "illumera" }. An admin turns the add-on on in Settings → Integrations, under Add-ons.
Request body
{
"email": "alice@example.com", // required
"name": "Alice Smith", // required, 1–255 characters
"role": "developer", // optional, defaults to "user"
"projectIds": ["proj-uuid-..."], // optional, projects to add the person to
"sendInvite": true, // optional, defaults to true (see below)
"externalRef": { // optional, your system's ID for this person
"provider": "illumera",
"externalType": "member",
"externalId": "illumera-member-42"
},
"metadata": {} // optional, stored on the integration link when externalRef is given
}
Every projectIds value must be a project in the organisation, or the request fails with 422 invite_failed.
sendInvite: invitation or direct creation
sendInvite | What happens for a new person |
|---|---|
true (default) | Align emails an invitation. No user exists until the person accepts, so the response has an inviteId and "user": null, "pending": true. Only the first project in projectIds is attached to the invitation. |
false | Align creates an active user straight away, with no email, and adds them to every project in projectIds. The response includes the user. |
The role field
If role is omitted, the person gets user. These values are accepted:
| Role | Can approve time entries |
|---|---|
viewer | No |
user (default) | No |
client | No |
tester | No |
developer | No |
tech_lead | No |
product_owner | No |
devops_engineer | No |
manager | Yes |
admin and platform_admin cannot be set through the API. Sending them, or any value not in the table, returns 403 with reason forbidden_role. To make someone an admin, provision them with another role, then have an existing admin change their role in Settings → Users.
Existing users keep their role
If the email already belongs to a user in the organisation, the endpoint returns that user and does not change their role. If you passed projectIds, they are added to those projects. PATCH /api/v1/users/{id} changes only name; it accepts metadata but does not store it, and it cannot change role.
If the email belongs to a user in a different organisation, the request fails with 422 invite_failed.
Response
The response body has the same shape in every case:
{
"userId": "user-uuid-...",
"inviteId": null,
"email": "alice@example.com",
"status": "created",
"pending": false,
"user": {
"id": "user-uuid-...",
"name": "Alice Smith",
"email": "alice@example.com",
"role": "developer",
"organizationId": "org-uuid-...",
"createdAt": "2026-10-14T09:00:00.000Z"
}
}
| HTTP | status | Meaning |
|---|---|---|
201 | created | A new user was created (sendInvite: false), or an invitation was sent (sendInvite: true, with inviteId and "pending": true) |
200 | linked | The person already existed (or the externalRef was already known). No user or membership change; if you sent externalRef, its link is recorded or updated. |
200 | updated | The person already existed and was added to projectIds |
422 | (reason invite_failed) | The email belongs to another organisation, a project ID is not in the organisation, or creation failed. message says which. |
Idempotent provisioning with externalRef
Pass externalRef to tie the person to your own ID. Align stores the mapping as an integration link. If you call again with the same externalRef, Align returns the person (or the pending invitation) it linked the first time instead of creating a duplicate.
curl -X POST https://app.alignsoft.us/api/v1/users/invite \
-H "Authorization: Bearer $ALIGN_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"name": "Alice Smith",
"role": "developer",
"externalRef": {
"provider": "illumera",
"externalType": "member",
"externalId": "illumera-member-42"
}
}'
You can check the stored mappings later with GET /api/v1/integration-links?provider=illumera&externalType=member (see Reconciliation).
You can also send an Idempotency-Key header; see Idempotency.
Bulk provisioning
POST /api/v1/users/bulk-invite takes { "invites": [ ... ] }, where each item has the same fields as a single invite. A top-level sendInvite applies to every item that does not set its own; an item's sendInvite wins. When neither is set, an invitation is sent. More than 50 items returns 400 bulk_too_large. The response is always 200:
{
"results": [
{ "email": "alice@example.com", "status": "created", "userId": "user-uuid-...", "inviteId": null },
{ "email": "bob@example.com", "status": "failed", "error": "User email is associated with a different organization.", "userId": null, "inviteId": null }
],
"summary": { "total": 2, "created": 1, "linked": 0, "failed": 1 }
}
linked in the summary counts both linked and updated results. In a bulk request, a role that cannot be set produces a failed result for that item rather than a 403.
Keeping your copy in sync
Align does not currently send webhook events for users. If you keep a local copy of the user list, poll GET /api/v1/users regularly to pick up changes made in Align, such as role changes. See Reconciliation.