Summary and recommendation
Aha! exposes a REST API at https://<yourcompany>.aha.io/api/v1 supporting API key (Bearer token) or OAuth 2.0 authentication. Core user operations - list, get, create (scoped to a product/workspace), and update including deactivation - are available.
There is no DELETE endpoint for users; setting enabled: false via PUT /api/v1/users/:id is the only supported offboarding method. No native SCIM endpoint exists; the API is the closest available programmatic alternative. Stitchflow connects to Aha!
through an MCP server with ~100 deep IT/identity integrations, enabling automated provisioning and deprovisioning workflows without building or maintaining custom API logic.
API quick reference
| Has user API | Yes |
| Auth method | API Key (Bearer token) or OAuth 2.0 |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | Enterprise |
Authentication
Auth method: API Key (Bearer token) or OAuth 2.0
Setup steps
- Option A – API Key: Log in to Aha!, go to Settings → Personal → Developer, click 'Generate API key', name it, copy the key (shown only once). Pass as: Authorization: Bearer
. - Option B – OAuth 2.0: Register an OAuth2 application in Aha! (Settings → Personal → Developer → OAuth applications → Register OAuth application) to obtain client_id and client_secret.
- Redirect the user to https://
.aha.io/oauth/authorize?client_id=...&redirect_uri=...&response_type=code (authorization code flow) or response_type=token (implicit flow). - Exchange the returned code for an access token via POST https://
.aha.io/oauth/token with client_id, client_secret, code, grant_type=authorization_code, redirect_uri. - Use the returned access_token as: Authorization: Bearer
on all subsequent API requests. - Include a descriptive User-Agent header on every request, e.g. User-Agent: MyApp (admin@example.com).
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string (numeric 64-bit) | Unique Aha! user identifier. | system-generated | immutable | Used as path parameter for single-user endpoints. |
| string | User's email address; used as login identifier. | required | optional | ||
| first_name | string | User's first name. | required | optional | |
| last_name | string | User's last name. | required | optional | |
| enabled | boolean | Whether the user account is active. Set to false to deactivate. | optional | optional | Default is false in update presets; must be explicitly set to true to enable. |
| administrator | boolean | Whether the user has account-level administrator privileges. | optional | optional | Default is false. |
| role | string (enum) | Workspace-level role assigned to the user. Known values include 'viewer'. | optional | optional | Scoped to a specific product/workspace when creating via /api/v1/products/:product_id/users. |
| created_at | string (ISO 8601 datetime) | Timestamp when the user record was created. | system-generated | immutable | |
| updated_at | string (ISO 8601 datetime) | Timestamp of the last update to the user record. | system-generated | system-generated |
Core endpoints
List all users
- Method: GET
- URL:
https://company.aha.io/api/v1/users - Watch out for: Returns paginated results (default 30/page, max 200). Rate limit is per account, not per token.
Request example
curl -g "https://company.aha.io/api/v1/users" -X GET \
-H "Accept: application/json" \
-H "Authorization: Bearer <token>"
Response example
{
"pagination": {"total_records": 50, "total_pages": 2, "current_page": 1},
"users": [{"id": "123", "email": "user@example.com", "first_name": "Sam"}]
}
Get a specific user
- Method: GET
- URL:
https://company.aha.io/api/v1/users/:id - Watch out for: Returns 404 if the authenticated user does not have access to the requested record.
Request example
curl -g "https://company.aha.io/api/v1/users/1020675218" -X GET \
-H "Accept: application/json" \
-H "Authorization: Bearer <token>"
Response example
{
"user": {
"id": "1020675218",
"email": "sam@example.com",
"first_name": "Sam",
"last_name": "Doe",
"enabled": true,
"administrator": false
}
}
Create a user (invite to workspace)
- Method: POST
- URL:
https://company.aha.io/api/v1/products/:product_id/users - Watch out for: User creation triggers an email invitation. Scoped to a specific product/workspace. No global account-level user creation endpoint documented.
Request example
curl "https://company.aha.io/api/v1/products/PRJ1/users" \
-d '{"user":{"email":"sam@example.com","first_name":"sam","last_name":"doe","role":"viewer"}}' \
-X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json"
Response example
{
"user": {
"id": "9876543210",
"email": "sam@example.com",
"first_name": "sam",
"last_name": "doe",
"role": "viewer"
}
}
Update a user (including deactivate)
- Method: PUT
- URL:
https://company.aha.io/api/v1/users/:id - Watch out for: Setting enabled: false deactivates the user. The numeric user id is required in the URL path.
Request example
curl "https://company.aha.io/api/v1/users/1020675218" \
-d '{"user":{"first_name":"Sarah","enabled":false}}' \
-X PUT -H "Authorization: Bearer <token>" -H "Content-Type: application/json"
Response example
{
"user": {
"id": "1020675218",
"first_name": "Sarah",
"enabled": false
}
}
List portal users (Ideas portal)
- Method: GET
- URL:
https://company.aha.io/api/v1/idea_portals/:idea_portal_id/portal_users - Watch out for: Portal users (Ideas portal) are separate from core Aha! workspace users and managed under a different resource path.
Request example
curl -g "https://company.aha.io/api/v1/idea_portals/1070474755/portal_users" -X GET \
-H "Authorization: Bearer <token>" -H "Accept: application/json"
Response example
{
"portal_users": [{"id": "646391926", "email": "user@example.com"}]
}
Create a portal user (Ideas portal)
- Method: POST
- URL:
https://company.aha.io/api/v1/idea_portals/:idea_portal_id/portal_users - Watch out for: permission field controls portal access level (e.g., 'employee'). Separate from workspace roles.
Request example
curl "https://company.aha.io/api/v1/idea_portals/1070474755/portal_users" \
-d '{"portal_user":{"email":"sam@example.com","first_name":"sam","last_name":"doe","permission":"employee"}}' \
-X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json"
Response example
{
"portal_user": {
"id": "999001",
"email": "sam@example.com",
"permission": "employee"
}
}
Update a portal user (Ideas portal)
- Method: PUT
- URL:
https://company.aha.io/api/v1/idea_portals/:idea_portal_id/portal_users/:id - Watch out for: Both the idea_portal_id and portal_user id are required in the path.
Request example
curl "https://company.aha.io/api/v1/idea_portals/1070474755/portal_users/646391926" \
-d '{"portal_user":{"first_name":"Sarah"}}' \
-X PUT -H "Authorization: Bearer <token>" -H "Content-Type: application/json"
Response example
{
"portal_user": {
"id": "646391926",
"first_name": "Sarah"
}
}
Validate webhook endpoint
- Method: GET
- URL:
https://company.aha.io/api/v1/webhooks/:callback_token - Watch out for: Webhooks use callback tokens, not standard Bearer auth. Used to validate webhook URL connectivity during setup.
Request example
curl -g "https://company.aha.io/api/v1/webhooks/22b7893e7fa1c4c60847090f78fbf0ec" -X GET \
-H "Authorization: Bearer <token>"
Response example
{ "status": "ok" }
Rate limits, pagination, and events
- Rate limits: Rate limits are applied per account (all API users in the same account share the limit). Exceeding either limit returns HTTP 429.
- Rate-limit headers: Yes
- Retry-After header: No
- Rate-limit notes: Response headers on rate-limited requests: X-Ratelimit-Limit, X-Ratelimit-Remaining, X-Ratelimit-Reset (UTC unix timestamp when limit resets). No Retry-After header; use X-Ratelimit-Reset to determine when to retry.
- Pagination method: offset
- Default page size: 30
- Max page size: 200
- Pagination pointer: page / per_page
| Plan | Limit | Concurrent |
|---|---|---|
| All plans | 300 requests/minute AND 20 requests/second | 0 |
- Webhooks available: Yes
- Webhook notes: Aha! supports two outbound webhook types: (1) Activity webhooks - send workspace or account-level record change events to a third-party URL; configurable at account or workspace level via Settings → Integrations. Activity webhooks only send updated fields, not full records, and have a 5-minute delay. (2) Audit webhooks - provide a live stream of all events (create, update, destroy) occurring within Aha! for custom logic or compliance auditing. Inbound webhooks (from external tools like Salesforce, GitHub, etc.) are also supported via callback tokens.
- Alternative event strategy: Poll GET /api/v1/users and related endpoints for user-state changes if webhook coverage for user events is insufficient.
- Webhook events: audit (create), audit (update), audit (destroy), activity (record type changes - features, releases, epics, etc.), activity (field-level changes), activity (workspace-level changes)
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Enterprise
- Endpoint: Not documented
Limitations:
- No native SCIM support; feature has been requested by users.
- JIT (Just-in-Time) provisioning via SAML SSO is supported but creates users with no default role or workspace access.
- Administrators must manually assign roles and workspace access after JIT provisioning.
- Custom SAML attributes (e.g., ProductPrefix/ProductRole) can be used for role mapping workarounds.
- API-based user management (REST) is available as an alternative to SCIM for provisioning/deprovisioning.
Common scenarios
Three primary automation scenarios are supported by the API. First, deprovisioning a leaver: retrieve the user's numeric id via GET /api/v1/users (match on email), then call PUT /api/v1/users/:id with {"user":{"enabled":false}}; confirm enabled: false in the response.
Second, provisioning a new user: POST to /api/v1/products/:product_id/users with email, first_name, last_name, and role - this triggers an email invitation automatically, so batch imports should be sequenced carefully.
Third, roster sync: paginate GET /api/v1/users (default 30/page, max 200 via per_page) using pagination. total_pages, collect id, email, enabled, and administrator fields, diff against your directory, and issue PUT calls for discrepancies.
Workspace users and Ideas portal users are separate resources with distinct endpoints and permission models - do not conflate them.
Deprovision a leaver (disable user account)
- Identify the user's Aha! numeric id by calling GET /api/v1/users?page=1&per_page=200 and matching on email.
- Call PUT /api/v1/users/:id with body {"user":{"enabled":false}} using an admin-level API key.
- Verify the response returns enabled: false to confirm deactivation.
Watch out for: There is no DELETE endpoint for users - deactivation via enabled: false is the supported offboarding method. The user id (not email) is required in the URL.
Provision a new user to a workspace
- Obtain the target workspace's product_id (e.g., PRJ1) from the Aha! UI or GET /api/v1/products.
- Call POST /api/v1/products/:product_id/users with body {"user":{"email":"...","first_name":"...","last_name":"...","role":"viewer"}}.
- The user receives an email invitation; an admin must manually assign additional workspace roles or permissions if needed.
- Store the returned user id for future update or deactivation calls.
Watch out for: User creation sends an email invite automatically - suppress or batch carefully during bulk imports. No global account-level user creation endpoint exists; provisioning is always scoped to a product/workspace.
Sync user roster to an external directory
- Call GET /api/v1/users?page=1&per_page=200 and iterate through all pages using the pagination.total_pages field.
- For each page, collect id, email, first_name, last_name, enabled, administrator fields.
- Compare against your directory source of truth and identify discrepancies.
- For users to deactivate, call PUT /api/v1/users/:id with {"user":{"enabled":false}}.
- Respect the 300 req/min and 20 req/sec rate limits; back off on HTTP 429 using X-Ratelimit-Reset.
Watch out for: All API users in the same Aha! account share the rate limit pool. A sync job running alongside other integrations may exhaust the limit faster than expected.
Why building this yourself is a trap
Several non-obvious constraints affect API-based user management in Aha!. Rate limits are enforced per account, not per token - all integrations sharing an account pool the same 300 req/min and 20 req/sec ceiling; a sync job running alongside other integrations can exhaust the limit faster than expected.
HTTP 429 responses do not include a Retry-After header; use X-Ratelimit-Reset (UTC unix timestamp) to determine backoff timing. API keys are tied to a specific user, not a service account - all actions are attributed to that user, and key rotation requires manual reissuance.
Creating users via the API sends email invitations by default with no documented suppression flag, which is disruptive during bulk imports. Custom fields are omitted from list endpoint responses by default; use the fields= query parameter to include them.
Finally, there is no global account-level user creation endpoint - provisioning is always scoped to a specific product/workspace, meaning multi-workspace onboarding requires multiple POST calls.
Automate Aha! workflows without one-off scripts
Stitchflow builds and maintains identity workflows for your exact setup. We cover every app, including the ones without APIs, and run deterministic trigger-to-report workflows with human approvals where they matter.