Summary and recommendation
The X Ads API exposes a dedicated account_users sub-resource under each ads account for programmatic user provisioning and deprovisioning.
All write operations require OAuth 1.0a with user-context;
OAuth 2.0 Bearer Token (app-only) is explicitly unsupported for account_users mutations.
Ads API access is not automatically available - it requires explicit approval from X before any integration work can begin.
Integrating X Ads into an identity graph requires resolving Twitter user_ids via the Twitter API v2 user lookup endpoint before any provisioning call, since the Ads API operates on numeric user_ids, not usernames or email addresses.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 1.0a (user-context required for Ads API calls; OAuth 2.0 Bearer Token is not supported for Ads API write operations) |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | N/A |
Authentication
Auth method: OAuth 1.0a (user-context required for Ads API calls; OAuth 2.0 Bearer Token is not supported for Ads API write operations)
Setup steps
- Apply for Ads API access at developer.twitter.com and receive approval from the X Ads API team.
- Create a Project and App in the X Developer Portal to obtain a Consumer Key and Consumer Secret.
- Implement the OAuth 1.0a three-legged flow to obtain an Access Token and Access Token Secret on behalf of the ads account owner.
- Include OAuth 1.0a Authorization header (with oauth_consumer_key, oauth_token, oauth_signature, etc.) on every API request.
- Ensure the authenticated user has the appropriate role on the target Ads Account (e.g., ACCOUNT_MANAGER or higher).
Required scopes
| Scope | Description | Required for |
|---|---|---|
| ads:read | Read access to ads account data including users, campaigns, and analytics. | GET operations on account users and account metadata |
| ads:write | Write access to ads account data including creating and modifying account users. | POST/PUT/DELETE operations on account users |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string | Unique identifier for the account user association. | system-generated | immutable | Ads API user ID, distinct from the Twitter user ID. |
| user_id | string | The Twitter user ID of the user being granted access. | required | immutable | Must correspond to an existing Twitter account. |
| account_id | string | The Ads account ID this user association belongs to. | required | immutable | Ads account IDs are base-36 encoded strings. |
| permission_level | enum | The role/permission level of the user on the account. | required | updatable | Values: ACCOUNT_MANAGER, CAMPAIGN_ANALYST, ORGANIC_ANALYST, CREATIVE_MANAGER, DSO_ADVERTISER. |
| scope | enum | Scope of access: ACCOUNT (full account) or CAMPAIGN (specific campaigns). | required | updatable | CAMPAIGN scope requires specifying campaign_ids. |
| campaign_ids | array[string] | List of campaign IDs the user has access to when scope is CAMPAIGN. | conditional | updatable | Only applicable when scope=CAMPAIGN. |
| created_at | datetime | Timestamp when the user association was created. | system-generated | immutable | ISO 8601 format. |
| updated_at | datetime | Timestamp when the user association was last updated. | system-generated | system-generated | ISO 8601 format. |
| deleted | boolean | Whether the user association has been deleted/revoked. | false | set to true on DELETE | Soft-delete pattern; deleted records may still appear in responses with deleted=true. |
Core endpoints
List account users
- Method: GET
- URL:
https://ads-api.twitter.com/12/accounts/{account_id}/account_users - Watch out for: Returns both active and soft-deleted users unless filtered. Use with_deleted=false to exclude deleted associations.
Request example
GET /12/accounts/abc123/account_users
Authorization: OAuth oauth_consumer_key="...", ...
Response example
{
"data": [{"id":"xyz","user_id":"123","permission_level":"ACCOUNT_MANAGER","scope":"ACCOUNT","deleted":false}],
"total_count": 1
}
Get a single account user
- Method: GET
- URL:
https://ads-api.twitter.com/12/accounts/{account_id}/account_users/{account_user_id} - Watch out for: The account_user_id is the Ads API association ID, not the Twitter user_id.
Request example
GET /12/accounts/abc123/account_users/xyz
Authorization: OAuth oauth_consumer_key="...", ...
Response example
{
"data": {"id":"xyz","user_id":"123","permission_level":"CAMPAIGN_ANALYST","scope":"ACCOUNT","deleted":false}
}
Add user to account
- Method: POST
- URL:
https://ads-api.twitter.com/12/accounts/{account_id}/account_users - Watch out for: The user_id must be a valid Twitter account. The authenticating user must have ACCOUNT_MANAGER permission or higher on the target account.
Request example
POST /12/accounts/abc123/account_users
Content-Type: application/x-www-form-urlencoded
user_id=456&permission_level=CAMPAIGN_ANALYST&scope=ACCOUNT
Response example
{
"data": {"id":"new_id","user_id":"456","permission_level":"CAMPAIGN_ANALYST","scope":"ACCOUNT","deleted":false}
}
Update account user permissions
- Method: PUT
- URL:
https://ads-api.twitter.com/12/accounts/{account_id}/account_users/{account_user_id} - Watch out for: Full replacement semantics on PUT; omitting optional fields may reset them to defaults.
Request example
PUT /12/accounts/abc123/account_users/xyz
Content-Type: application/x-www-form-urlencoded
permission_level=ACCOUNT_MANAGER&scope=ACCOUNT
Response example
{
"data": {"id":"xyz","user_id":"456","permission_level":"ACCOUNT_MANAGER","scope":"ACCOUNT","deleted":false}
}
Remove user from account
- Method: DELETE
- URL:
https://ads-api.twitter.com/12/accounts/{account_id}/account_users/{account_user_id} - Watch out for: Soft-delete only; the record remains retrievable with deleted=true. The user loses access immediately upon deletion.
Request example
DELETE /12/accounts/abc123/account_users/xyz
Authorization: OAuth oauth_consumer_key="...", ...
Response example
{
"data": {"id":"xyz","deleted":true}
}
Get authenticated user's account access
- Method: GET
- URL:
https://ads-api.twitter.com/12/accounts/{account_id} - Watch out for: Returns account metadata, not user list. Use /account_users sub-resource for user enumeration.
Request example
GET /12/accounts/abc123
Authorization: OAuth oauth_consumer_key="...", ...
Response example
{
"data": {"id":"abc123","name":"My Ads Account","timezone":"America/New_York","deleted":false}
}
Rate limits, pagination, and events
- Rate limits: The Ads API enforces per-endpoint rate limits. Limits vary by endpoint and are documented per resource. Requests exceeding limits receive HTTP 429 responses.
- Rate-limit headers: Yes
- Retry-After header: No
- Rate-limit notes: The Ads API returns rate limit information in response headers: x-rate-limit, x-rate-limit-remaining, and x-rate-limit-reset. Specific per-endpoint limits are listed in the official API reference. The docs do not explicitly document a Retry-After header.
- Pagination method: cursor
- Default page size: 200
- Max page size: 1000
- Pagination pointer: cursor
| Plan | Limit | Concurrent |
|---|---|---|
| Standard Ads API Access | Varies by endpoint; commonly 100–1000 requests per 15-minute window per endpoint | 0 |
- Webhooks available: No
- Webhook notes: The X Ads API does not offer webhooks for user-management events such as user addition, removal, or permission changes. The X Account Activity API (for organic Twitter activity) is separate and does not cover Ads account user events.
- Alternative event strategy: Poll the GET /accounts/{account_id}/account_users endpoint periodically to detect changes in account user membership and permissions.
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: N/A
- Endpoint: Not documented
Limitations:
- X Ads does not provide a SCIM 2.0 endpoint.
- No native IdP (Okta, Entra ID, Google Workspace, OneLogin) provisioning connectors are available for X Ads.
- User provisioning must be performed directly via the Ads API account_users endpoints.
Common scenarios
Three core automation scenarios are well-supported by the API.
First, onboarding: resolve the target user's Twitter user_id via GET /2/users/by/username/:username, then POST to /12/accounts/{account_id}/account_users with permission_level and scope;
confirm deleted=false in the response.
Second, access auditing: GET /12/accounts/{account_id}/account_users?with_deleted=false and paginate via the cursor parameter (default page size 200, max 1000) until next_cursor is null, logging user_id, permission_level, scope, and campaign_ids per record.
Third, permission downgrade: retrieve the account_user_id via GET, then PUT the full user object with the revised permission_level PUT uses full-replacement semantics, so omitting any field may reset it to a default.
No webhooks exist for user-management events;
change detection requires polling.
Onboard a new team member to an Ads account
- Obtain the new user's Twitter user_id (via Twitter API v2 GET /2/users/by/username/:username).
- Authenticate via OAuth 1.0a as an existing ACCOUNT_MANAGER on the target ads account.
- POST to /12/accounts/{account_id}/account_users with user_id, permission_level, and scope.
- Confirm the response returns deleted=false and the expected permission_level.
Watch out for: The new user must have an existing Twitter account. You cannot create a Twitter account via the Ads API.
Audit all users on an Ads account
- Authenticate via OAuth 1.0a as an ACCOUNT_MANAGER.
- GET /12/accounts/{account_id}/account_users?with_deleted=false to retrieve active users.
- Paginate using the cursor parameter until next_cursor is null.
- For each user record, log user_id, permission_level, scope, and campaign_ids.
Watch out for: Default responses may include soft-deleted users; always pass with_deleted=false for active-only audits.
Downgrade a user's permission level
- GET /12/accounts/{account_id}/account_users to find the account_user_id for the target user.
- PUT /12/accounts/{account_id}/account_users/{account_user_id} with the new permission_level (e.g., CAMPAIGN_ANALYST) and existing scope.
- Verify the response reflects the updated permission_level.
Watch out for: PUT is a full replacement; include all required fields (scope, permission_level) in the request body to avoid unintended resets.
Why building this yourself is a trap
Several non-obvious constraints will surface during implementation. The API uses soft-deletes: removed account_user records remain in GET responses unless with_deleted=false is explicitly passed, which will silently inflate active-user counts in any identity graph sync that omits this filter. Account IDs are base-36 encoded strings, not integers - URL construction must handle this correctly.
The permission_level enum values are Ads API-specific and do not map directly to the UI role labels, requiring an explicit translation layer in any provisioning pipeline. CAMPAIGN-scoped access requires campaign_ids to be passed explicitly; omitting them when scope=CAMPAIGN returns an error.
API versioning is path-based (currently /12/); X periodically deprecates older versions, so version pinning without a changelog monitor is a maintenance risk. Finally, rate limits vary per endpoint and must be read from per-endpoint documentation - the x-rate-limit, x-rate-limit-remaining, and x-rate-limit-reset headers are present in responses, but no Retry-After header is documented.
Automate X Ads 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.