Summary and recommendation
The Heroku Platform API (v3) exposes full CRUD for Team and Enterprise Account membership via REST endpoints authenticated with OAuth 2.0 Bearer tokens or direct API keys. All requests require the `Accept: application/vnd.heroku+json; version=3` header - omitting it returns a 406.
Rate limits are enforced at 4500 requests/hour per token, with remaining quota surfaced in the `RateLimit-Remaining` response header.
Pagination uses HTTP Range headers rather than query parameters. The next page cursor is returned in the `Next-Range` response header; callers must re-issue the request with that value as the `Range` header to advance. Default page size is 200; callers can request up to 1000 per page using `max=1000` in the Range header.
Heroku has no native SCIM 2.0 endpoint. Automated provisioning and deprovisioning must be implemented directly against the Platform API team and enterprise member endpoints.
Okta, OneLogin, and Microsoft Entra ID integrations rely on SAML SSO with JIT provisioning - not SCIM - meaning deprovisioning is not handled by the IdP and must be driven by the Platform API or manual action.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 Bearer Token (also supports API Key via HTTP Basic Auth with token as password) |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | Enterprise |
Authentication
Auth method: OAuth 2.0 Bearer Token (also supports API Key via HTTP Basic Auth with token as password)
Setup steps
- Log in to Heroku and navigate to Account Settings > Applications.
- Create an OAuth Authorization or generate a direct API token via
heroku authorizations:createCLI command. - For OAuth 2.0 flow: register a client at https://api.heroku.com/oauth/clients, redirect user to https://id.heroku.com/oauth/authorize with desired scopes.
- Exchange authorization code for access token via POST https://id.heroku.com/oauth/token.
- Include token in requests as
Authorization: Bearer <token>header. - Set
Accept: application/vnd.heroku+json; version=3header on all requests.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| global | Full access to account, apps, and all resources. | All user management operations when using OAuth tokens. |
| read | Read-only access to account and app data. | Listing team members, reading account info. |
| write | Write access to account and app data. | Creating/updating team members and invitations. |
| read-protected | Read access to protected account fields (e.g., email). | Reading sensitive account fields. |
| write-protected | Write access to protected account fields. | Updating sensitive account fields. |
| identity | Read access to account identity information. | Fetching current user identity. |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string (UUID) | Unique identifier for the account. | system-generated | immutable | Used as stable identifier across API calls. |
| string | User's email address; used as login identifier. | required | updatable | Must be unique across Heroku. | |
| name | string | Full name of the account holder. | optional | updatable | |
| verified | boolean | Whether the account has a verified payment method. | system-set | read-only | Required for certain resource types. |
| created_at | datetime (ISO 8601) | Timestamp when the account was created. | system-generated | immutable | |
| updated_at | datetime (ISO 8601) | Timestamp of last account update. | system-generated | system-updated | |
| allow_tracking | boolean | Whether the user allows usage tracking. | optional | updatable | |
| beta | boolean | Whether the user is enrolled in beta features. | optional | updatable | |
| two_factor_authentication | boolean | Whether MFA/2FA is enabled on the account. | system-set | read-only via API | Managed via account settings UI. |
| delinquent_at | datetime|null | Timestamp when account became delinquent due to billing. | system-set | read-only | |
| role | string | Role of the member within a team (admin, member, viewer, collaborator). | required for team membership | updatable | Field present on team-member objects, not base account object. |
| federated | boolean | Whether the account is managed via SSO/federated identity. | system-set | read-only | Relevant for Enterprise accounts with SSO enabled. |
Core endpoints
Get current account info
- Method: GET
- URL:
https://api.heroku.com/account - Watch out for: Returns info for the token owner only. Cannot fetch arbitrary user accounts by ID without team/enterprise context.
Request example
GET /account HTTP/1.1
Host: api.heroku.com
Authorization: Bearer <token>
Accept: application/vnd.heroku+json; version=3
Response example
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"email": "user@example.com",
"name": "Jane Smith",
"verified": true,
"two_factor_authentication": true,
"created_at": "2024-01-15T10:00:00Z"
}
Update account
- Method: PATCH
- URL:
https://api.heroku.com/account - Watch out for: Only updates the authenticated user's own account. No admin endpoint to update arbitrary user accounts.
Request example
PATCH /account HTTP/1.1
Host: api.heroku.com
Authorization: Bearer <token>
Accept: application/vnd.heroku+json; version=3
Content-Type: application/json
{"name": "Jane Doe", "allow_tracking": false}
Response example
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"email": "user@example.com",
"name": "Jane Doe",
"allow_tracking": false
}
List team members
- Method: GET
- URL:
https://api.heroku.com/teams/{team_name_or_id}/members - Watch out for: Requires team admin role. Uses Range header for pagination, not query params.
Request example
GET /teams/my-team/members HTTP/1.1
Host: api.heroku.com
Authorization: Bearer <token>
Accept: application/vnd.heroku+json; version=3
Response example
[
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"email": "member@example.com",
"role": "member",
"federated": false,
"created_at": "2024-03-01T00:00:00Z"
}
]
Create/invite team member
- Method: PUT
- URL:
https://api.heroku.com/teams/{team_name_or_id}/members - Watch out for: Uses PUT (idempotent). If user does not have a Heroku account, an invitation email is sent. Role must be one of: admin, member, viewer, collaborator.
Request example
PUT /teams/my-team/members HTTP/1.1
Host: api.heroku.com
Authorization: Bearer <token>
Accept: application/vnd.heroku+json; version=3
Content-Type: application/json
{"email": "newuser@example.com", "role": "member"}
Response example
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"email": "newuser@example.com",
"role": "member",
"created_at": "2025-01-01T00:00:00Z"
}
Update team member role
- Method: PATCH
- URL:
https://api.heroku.com/teams/{team_name_or_id}/members - Watch out for: Email is used as the identifier in the body, not a path parameter.
Request example
PATCH /teams/my-team/members HTTP/1.1
Host: api.heroku.com
Authorization: Bearer <token>
Accept: application/vnd.heroku+json; version=3
Content-Type: application/json
{"email": "member@example.com", "role": "admin"}
Response example
{
"email": "member@example.com",
"role": "admin",
"updated_at": "2025-06-01T00:00:00Z"
}
Remove team member
- Method: DELETE
- URL:
https://api.heroku.com/teams/{team_name_or_id}/members/{member_email_or_id} - Watch out for: Removes user from team but does not delete their Heroku account. Does not revoke app collaborator access granted outside the team.
Request example
DELETE /teams/my-team/members/member@example.com HTTP/1.1
Host: api.heroku.com
Authorization: Bearer <token>
Accept: application/vnd.heroku+json; version=3
Response example
{
"email": "member@example.com",
"role": "member"
}
List enterprise account members
- Method: GET
- URL:
https://api.heroku.com/enterprise-accounts/{enterprise_account_name_or_id}/members - Watch out for: Requires Enterprise account. Endpoint path uses enterprise account name or UUID, not team name.
Request example
GET /enterprise-accounts/my-enterprise/members HTTP/1.1
Host: api.heroku.com
Authorization: Bearer <token>
Accept: application/vnd.heroku+json; version=3
Response example
[
{
"user": {"id": "abc123", "email": "admin@corp.com"},
"permissions": ["view", "manage"],
"federated": true
}
]
Create enterprise account member
- Method: POST
- URL:
https://api.heroku.com/enterprise-accounts/{enterprise_account_name_or_id}/members - Watch out for: Permissions array values differ from team roles. Valid values include: view, manage, billing. Enterprise plan required.
Request example
POST /enterprise-accounts/my-enterprise/members HTTP/1.1
Host: api.heroku.com
Authorization: Bearer <token>
Accept: application/vnd.heroku+json; version=3
Content-Type: application/json
{"email": "newadmin@corp.com", "permissions": ["view"]}
Response example
{
"user": {"id": "def456", "email": "newadmin@corp.com"},
"permissions": ["view"],
"created_at": "2025-06-01T00:00:00Z"
}
Rate limits, pagination, and events
- Rate limits: Heroku Platform API enforces rate limits per token. Standard limit is 4500 requests per hour per token.
- Rate-limit headers: Yes
- Retry-After header: No
- Rate-limit notes: Rate limit status returned in
RateLimit-Remainingheader. When limit is exceeded, API returns HTTP 429. Limits apply per authorization token, not per account. - Pagination method: cursor
- Default page size: 200
- Max page size: 1000
- Pagination pointer: Range header (e.g.,
Range: id ]<last_id>; max=200)
| Plan | Limit | Concurrent |
|---|---|---|
| All plans (per OAuth token) | 4500 requests/hour | 0 |
- Webhooks available: Yes
- Webhook notes: Heroku supports app-level webhooks via the Platform API. Webhooks can be configured to fire on specific events including API entity changes.
- Alternative event strategy: No dedicated user/member lifecycle webhook events (e.g., team member added/removed). Polling the team members endpoint is required for member change detection.
- Webhook events: api:addon-attachment, api:app, api:build, api:collaborator, api:domain, api:dyno, api:formation, api:release, api:sni-endpoint, api:ssl-endpoint
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Enterprise
- Endpoint: Not documented
Limitations:
- Heroku does not offer a native SCIM 2.0 API endpoint.
- SSO is supported via SAML with JIT (Just-In-Time) provisioning for Enterprise accounts.
- Automated provisioning/deprovisioning requires use of the Platform API team/enterprise member endpoints directly or via an IdP integration that calls the Platform API.
- Okta, OneLogin, and Microsoft Entra ID integrations use SAML SSO with JIT, not SCIM.
Common scenarios
Provisioning a new team member uses a PUT to /teams/{team}/members with email and role in the request body. The PUT is idempotent, so re-sending is safe.
If the target user has no existing Heroku account, Heroku sends an invitation email automatically - the member object is not fully active until the invitation is accepted, so callers should poll GET /teams/{team}/members to confirm membership state before treating provisioning as complete.
Deprovisioning requires a DELETE to /teams/{team}/members/{email_or_id}. This removes the user from the Team but does not delete their Heroku account and does not remove any direct app collaborator access granted outside the Team context. If the user was added as a collaborator on individual apps, each app must be cleaned up separately via DELETE /apps/{app}/collaborators/{email}. Personal API tokens and OAuth authorizations created by the removed user are also not invalidated automatically and must be revoked independently.
For identity graph construction across an Enterprise Account, call GET /enterprise-accounts/{enterprise}/members with enterprise admin credentials and paginate via Next-Range headers until exhausted. Each member object includes a federated boolean field that distinguishes SSO-managed accounts from direct accounts - useful for segmenting IdP-governed identities from manually provisioned ones. Cross-referencing this endpoint against per-Team member lists (which use a separate path prefix and separate pagination) is required to build a complete picture of user access across all Teams under an Enterprise Account.
Provision a new team member
- Authenticate and obtain a Bearer token with
globalorwritescope. - PUT https://api.heroku.com/teams/{team}/members with body
{"email": "user@example.com", "role": "member"}. - If user has no Heroku account, Heroku sends an invitation email automatically.
- Poll GET /teams/{team}/members to confirm membership status once invitation is accepted.
Watch out for: Member does not appear as fully active until they accept the invitation. The PUT is idempotent so re-sending is safe.
Deprovision / remove a team member
- Authenticate with a team admin token.
- DELETE https://api.heroku.com/teams/{team}/members/{email_or_id}.
- Verify removal by calling GET /teams/{team}/members and confirming the user is absent.
- If the user was also a direct app collaborator, separately remove them from each app via DELETE /apps/{app}/collaborators/{email}.
Watch out for: Removing a team member does not remove them as a direct app collaborator if they were added outside the team context. Both must be cleaned up separately.
Audit all members across an Enterprise account
- Authenticate with an enterprise account admin token.
- GET https://api.heroku.com/enterprise-accounts/{enterprise}/members with
Accept: application/vnd.heroku+json; version=3. - Check
Next-Rangeresponse header; if present, repeat request withRange: <Next-Range value>header to fetch next page. - Continue until no
Next-Rangeheader is returned. - For each member, cross-reference
federatedfield to identify SSO-managed vs. direct accounts.
Watch out for: Default page size is 200; use max=1000 in Range header to reduce round trips. Enterprise endpoint is separate from team-level member endpoints.
Why building this yourself is a trap
The most significant API-layer trap is the identifier inconsistency across endpoints: PATCH operations on team members use email address as the identifier in the request body, while DELETE operations accept either email or UUID in the path. Callers building generic provisioning logic must handle this per-endpoint rather than assuming a uniform identifier strategy.
A second trap is the invitation-pending state. A PUT to create a team member returns a 200 and a member object, but the user is not active until they accept the invitation email.
Downstream logic that assumes immediate activation - such as granting app-level access immediately after provisioning - will operate on a member who cannot yet authenticate. There is no webhook event for invitation acceptance; polling is the only detection mechanism.
Enterprise Account endpoints use a separate path prefix (/enterprise-accounts/) and a different permissions model from Team endpoints. Valid permission values for Enterprise members (view, manage, billing) do not map to Team role strings (admin, member, viewer). Conflating the two models in shared provisioning code is a common source of 422 errors.
Additionally, there are no dedicated member lifecycle webhook events - no signal fires when a team member is added or removed - so any event-driven identity graph that depends on Heroku membership changes must rely entirely on scheduled polling of the member endpoints.
Automate Heroku workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.