Users API
Provision accounts on Bodek programmatically: create individual end-users, create business accounts with their own organization workspace, invite people to organization workspaces, and send password-reset emails for accounts you created. It's the building block for partners who onboard their own customers onto the platform.
Base https://dev.bodek.us/v1/users
Auth Authorization: Bearer bf_live_… (a key created for the "users" service)
| Endpoint | Scope | Purpose |
|---|---|---|
POST /v1/users | users:create | Create an individual account |
POST /v1/users/business | business:create | Create a business account + organization workspace |
POST /v1/users/workspace-members | workspace:invite | Invite someone to an organization workspace |
POST /v1/users/password-reset | users:password_reset | Email a password-reset link to an account you created |
This reference is public. Creating a key, however, requires approval — see below.
The Users API is served by dev.bodek.us itself; there is no separate direct host.
Getting access
The Users API is approval-gated to keep account creation safe. Before you can create a key:
- Submit the API access request with your use case.
- Once an administrator approves it (you'll be emailed), a Users option appears when you create a key in the console.
- Create a Users key, choosing the scopes it needs, and start calling the API.
Approval is checked on every request: if it's revoked, existing keys stop working
immediately with 403 access_not_granted. Every call is recorded in your key's audit log.
Authentication
Send your key as a bearer token. Keys for other services (files, forms, iam) are rejected
with 403 wrong_service.
Authorization: Bearer bf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
If the key has an allowed-domains list, browser requests must come from one of those origins (see Origin restrictions). Because these endpoints create accounts and send email, call them from your server, never from a browser.
Requests & responses
- Every endpoint is
POSTwith a JSON body. Other methods return405 method_not_allowed(after the key is authenticated). - Successful calls return 201 with the result object
at the top level — unlike the Files and Forms APIs, there is no
dataenvelope. - Errors use the standard envelope:
{"error": {"code": "…", "message": "…", "details": {…}}}. - Each response carries an
X-Request-Idheader; quote it in support requests. - Emails are trimmed and lower-cased. Names are trimmed and truncated to 80 characters, phone to 40.
- There are no read, list, update or delete endpoints.
Create a user
Scope users:create. Creates an individual account plus its personal workspace
(5 GB storage) and queues a welcome email plus a single-use link (valid 72 hours) for the user to
choose their own password. Integrations never set or see end-user passwords.
| Field | Description | |
|---|---|---|
email | required | Valid, unique email address. |
first_name | required | Up to 80 characters. |
last_name | required | Up to 80 characters. |
phone | optional | Free text, up to 40 characters (stored encrypted). |
curl -X POST https://dev.bodek.us/v1/users \
-H "Authorization: Bearer bf_live_…" \
-H "Content-Type: application/json" \
-d '{
"email": "jordan@example.com",
"first_name": "Jordan",
"last_name": "Lee",
"phone": "+1 555 0100"
}'
Returns 201:
{
"id": 8123,
"email": "jordan@example.com",
"first_name": "Jordan",
"last_name": "Lee",
"account_type": "individual",
"workspace_id": 9001,
"email_verified": false
}
Sending a password field is rejected with 400 unsupported_parameter.
If the link expires, use Send a password reset.
Accounts created through the API are recorded as created by your account (which is what allows you to send them password-reset emails later) and are sponsored by the key owner for billing purposes.
Create a business
Scope business:create. Creates a business account with its personal workspace, plus an
organization workspace (10 GB storage) owned by that user, and queues a welcome email. Takes the same
fields as Create a user, plus:
| Field | Description | |
|---|---|---|
organization_name | required | Name of the organization workspace. org_name is accepted as an alias. |
ein | optional | US employer id, 12-3456789 or 123456789. |
curl -X POST https://dev.bodek.us/v1/users/business \
-H "Authorization: Bearer bf_live_…" \
-H "Content-Type: application/json" \
-d '{
"email": "owner@acme.co",
"first_name": "Sam",
"last_name": "Rivera",
"organization_name": "Acme Co",
"ein": "12-3456789"
}'
{
"id": 8124,
"email": "owner@acme.co",
"first_name": "Sam",
"last_name": "Rivera",
"account_type": "business",
"organization": { "workspace_id": 9002, "name": "Acme Co" },
"email_verified": false
}
Add a user to a workspace
Scope workspace:invite. Emails an invitation to join an organization workspace. The
key's owner must be the owner or an admin of the target workspace, and it must be an
organization (not a personal) workspace. The invitee doesn't need a Bodek account yet.
| Field | Description | |
|---|---|---|
workspace_id | required | Organization workspace id (e.g. organization.workspace_id from Create a business). |
email | required | Invitee's email. |
role | optional | member (default) or admin. |
curl -X POST https://dev.bodek.us/v1/users/workspace-members \
-H "Authorization: Bearer bf_live_…" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": 9002,
"email": "teammate@acme.co",
"role": "member"
}'
{
"workspace_id": 9002,
"email": "teammate@acme.co",
"role": "member",
"status": "invited",
"invited_at": "2026-09-23T15:40:00+00:00"
}
The invitation link is valid for 14 days; the person joins when they accept it.
Inviting an email that already has a pending (or removed) membership re-issues the invitation with the
new role and a fresh link. An email that is already an active member returns
409 already_member.
Send a password reset
Scope users:password_reset. Emails a single-use link (valid 30 minutes) to an account that
your integration created (any key belonging to your developer account), so the user can
choose a new password. The API never sets or returns passwords, and you can't target arbitrary Bodek
accounts. At most 5 reset emails per address per hour.
| Field | Description | |
|---|---|---|
user_id | one of | The account id returned at creation. |
email | one of | Used when user_id isn't given. |
curl -X POST https://dev.bodek.us/v1/users/password-reset \
-H "Authorization: Bearer bf_live_…" \
-H "Content-Type: application/json" \
-d '{ "email": "jordan@example.com" }'
{ "id": 8123, "email": "jordan@example.com", "reset": "email_sent" }
An account that doesn't exist and one your integration didn't create both return
404 user_not_found. Sending new_password is rejected with
400 unsupported_parameter.
Scopes
Choose scopes when creating a Users key in the console.
| Scope | Grants |
|---|---|
users:create | POST /v1/users — create individual end-user accounts. |
business:create | POST /v1/users/business — create business accounts with an organization workspace. |
workspace:invite | POST /v1/users/workspace-members — invite members to organization workspaces the key owner administers. |
users:password_reset | POST /v1/users/password-reset — email a password-reset link to accounts your integration created. |
Rate limits
Each key has the per-minute and per-day caps chosen when it was created, but Users keys are capped at
30/minute and 1,000/day. On top of that, all Users keys of one developer account share a ceiling of
60 requests/minute and 2,000/day, and at most 200 new accounts per rolling 24 hours
(429 account_creation_limit). Re-inviting the same address to the same workspace within
10 minutes returns 429 invite_recently_sent. Every request, including failed ones, counts. Responses that pass authentication carry
X-RateLimit-Limit-Minute, X-RateLimit-Remaining-Minute,
X-RateLimit-Limit-Day and X-RateLimit-Remaining-Day. Over the limit you get
429 rate_limited with error.details.limit.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json | The body isn't valid JSON. |
| 400 | invalid_email | Missing or invalid email. |
| 400 | missing_name | first_name and last_name are required. |
| 400 | unsupported_parameter | password / new_password sent — users set their own passwords via the emailed link. |
| 400 | missing_organization | organization_name is required for a business. |
| 400 | invalid_ein | EIN isn't 9 digits (12-3456789). |
| 400 | missing_workspace | workspace_id is required. |
| 400 | invalid_role | role must be member or admin. |
| 400 | not_an_org_workspace | Members can only be added to organization workspaces. |
| 400 | missing_user | Provide user_id or email. |
| 401 | missing_authorization | No Authorization: Bearer header. |
| 401 | invalid_token_format | Not a bf_live_… / bf_test_… key. |
| 401 | invalid_token | Key not recognized. |
| 401 | token_revoked / token_expired | The key was revoked or has expired. |
| 403 | wrong_service | The key belongs to another service. |
| 403 | origin_blocked / origin_required / ip_blocked | The request doesn't satisfy the key's origin restrictions. |
| 403 | access_not_granted | No active Users API approval (not approved yet, or revoked). |
| 403 | insufficient_scope | The key lacks the endpoint's scope (error.details.required). |
| 404 | not_found | No Users API endpoint at that path. |
| 404 | workspace_not_found | The workspace doesn't exist, or the key owner isn't an owner/admin of it. |
| 404 | user_not_found | No account created by your integration matches (also returned for accounts you didn't create). |
| 405 | method_not_allowed | Only POST is accepted. |
| 409 | email_taken | An account with that email already exists. |
| 409 | already_member | That email is already an active member of the workspace. |
| 413 | payload_too_large | Request body over 64 KB. |
| 429 | rate_limited | Per-key or account-wide per-minute / per-day limit exceeded (also: too many reset emails for one address). |
| 429 | account_creation_limit / invite_recently_sent | Daily new-account ceiling reached, or the same invite was sent in the last 10 minutes. |
| 503 | rate_limit_unavailable | Limits couldn't be checked; nothing was done — retry. |
| 500 | server_error | Unexpected error — retry, and quote the X-Request-Id if it persists. |