Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

The Okta API: Tokens, Scopes, and the Rate Limit That Catches Everyone

Sean

Platform Writer

Aug 14, 2026
8 min read

The Okta management API has two authentication models and the choice matters more than the documentation suggests. SSWS API tokens are simple, inherit the full permissions of the user who made them, and are what most integrations start with. OAuth client credentials with scoped access are what production integrations should use, because a leaked SSWS token is administrative access to your entire identity provider.

The Okta API: Tokens, Scopes, and the Rate Limit That Catches Everyone

Okta’s API is well documented endpoint by endpoint, which makes it easy to make a first call and easy to build something you later have to rewrite. The parts worth deciding up front are authentication, pagination, and rate limits.

Table of contents

The two authentication models

An SSWS token is created in the admin console and passed in an Authorization header. It is a single string with no expiry and no scoping.

curl -X GET "https://your-org.okta.com/api/v1/users?limit=200" \
  -H "Accept: application/json" \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}"

The properties to understand: it carries the permissions of the administrator who created it, so a token made by a super administrator can do anything including creating other administrators. It becomes inactive if unused for 30 days. And it is deactivated if the creating user is deactivated, which is how integrations break when someone leaves.

The OAuth approach registers a service application, grants it specific scopes, and exchanges a signed assertion for a short-lived access token.

curl -X POST "https://your-org.okta.com/oauth2/v1/token" \
  -H "Accept: application/json" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "scope=okta.users.read okta.groups.read" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  -d "client_assertion=${SIGNED_JWT}"

# Then use the returned access token.
curl -X GET "https://your-org.okta.com/api/v1/users" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

More setup, and worth it. The token is scoped to exactly the operations you granted, expires in an hour, and is tied to an application rather than a person, so it survives staffing changes. The choice is between a credential that can do everything forever and one that can do specific things briefly.

Pagination, which is not offset-based

Okta uses cursor pagination through the HTTP Link header, and there is no page number and no total count. Code that assumes a page parameter will silently process only the first batch.

import requests, re

def fetch_all(url, headers):
    while url:
        r = requests.get(url, headers=headers)
        r.raise_for_status()
        yield from r.json()

        # The next page is in the Link header, if there is one.
        url = None
        for link in r.headers.get("Link", "").split(","):
            m = re.match(r'\s*<([^>]+)>;\s*rel="next"', link)
            if m:
                url = m.group(1)
                break

headers = {"Authorization": f"SSWS {token}", "Accept": "application/json"}
for user in fetch_all("https://your-org.okta.com/api/v1/users?limit=200", headers):
    print(user["profile"]["email"])

Two details. Always set limit explicitly, because the default is small and more requests against a rate limit is the thing you are trying to avoid. Maximum is 200 for most collection endpoints.

And never construct the next URL yourself. It contains an opaque cursor, and rebuilding it from parameters you recognise produces incorrect results or an infinite loop. Follow the header exactly as given.

There is deliberately no total count, so a progress bar based on percentage is not available. Count as you go.

Rate limits, and the header that prevents surprises

Okta enforces per-endpoint rate limits on a rolling one-minute window, and the limits vary considerably by endpoint and by subscription level. The list endpoints are generous; the ones performing writes or searches are much tighter.

Every response carries the current state, and reading it is what separates an integration that works from one that works until a busy day.

X-Rate-Limit-Limit: 600
X-Rate-Limit-Remaining: 412
X-Rate-Limit-Reset: 1755180000

The reset value is a Unix timestamp for when the window rolls over. On exceeding the limit you get a 429 with an error code of E0000047, and the correct response is to wait until that timestamp rather than retry immediately.

import time

def request_with_backoff(session, method, url, **kwargs):
    for attempt in range(5):
        r = session.request(method, url, **kwargs)

        if r.status_code == 429:
            reset = int(r.headers.get("X-Rate-Limit-Reset", 0))
            wait = max(reset - time.time(), 2 ** attempt)
            time.sleep(min(wait + 1, 60))
            continue

        # Slow down before hitting the wall.
        remaining = int(r.headers.get("X-Rate-Limit-Remaining", 999))
        if remaining < 20:
            time.sleep(1)

        r.raise_for_status()
        return r

    raise RuntimeError("rate limited after 5 attempts")

The second half of that function matters as much as the first. Reacting only to 429s means you repeatedly hit the wall and stall. Slowing down when the remaining count gets low keeps throughput steady.

Also worth knowing: limits are per organisation, not per token. A batch job consuming the quota will rate limit the login flow of your actual users, which is the failure that turns a slow script into an incident. Run bulk work off-peak and deliberately throttled.

Searching and filtering without scanning everything

Fetching all users and filtering client-side works until you have thousands, then it is slow and consumes your rate limit. Okta offers three query mechanisms with different behaviour.

# q: simple prefix match on name and email. Fast, limited.
curl -G "https://your-org.okta.com/api/v1/users" --data-urlencode 'q=jane'

# filter: SCIM expression. Indexed, supports a limited set of properties.
curl -G "https://your-org.okta.com/api/v1/users" \
  --data-urlencode 'filter=status eq "ACTIVE" and profile.department eq "Sales"'

# search: broader, supports custom attributes and more operators.
curl -G "https://your-org.okta.com/api/v1/users" \
  --data-urlencode 'search=profile.employeeNumber sw "E1" and status eq "ACTIVE"' \
  --data-urlencode 'sortBy=profile.lastName'

Use search for anything non-trivial. It covers custom profile attributes, supports sorting, and handles more operators than filter. The trade is that it is eventually consistent, so a user created a moment ago may not appear immediately, which produces confusing test failures in a create-then-search sequence.

filter is strongly consistent but restricted to a specific set of indexed properties, and asking it about anything else returns an error rather than falling back.

Quoting is the usual source of 400 responses here. String values need double quotes inside the expression, which means careful escaping in a shell and letting your HTTP library handle the encoding in code.

Practical guidance

  1. Use OAuth with scoped service applications for anything in production. Reserve SSWS tokens for local experimentation.
  2. Grant the narrowest scopes that work. Read scopes for anything that only reads, and never a super administrator token for a reporting job.
  3. Store credentials in environment variables, never in the repository, and rotate on any suspicion of exposure.
  4. Follow the Link header for pagination and set limit explicitly at 200.
  5. Read the rate limit headers on every response and slow down before you are stopped.
  6. Run bulk operations off-peak, since the quota is shared with your users’ authentication traffic.
  7. For continuous user provisioning, evaluate SCIM or event hooks before writing a polling loop, since polling an identity provider on a schedule is the pattern that most often exhausts a rate limit.

That last point is the design-level version of the rate limit advice. A great many integrations poll for changes when the platform offers to push them, and the push version is both cheaper and closer to real time.

How this fits the rest of the stack

An integration like this is a service: it needs somewhere to run, credentials held outside the repository, and logs showing which calls were made and which were rate limited. Event hooks in particular need a publicly reachable endpoint with a certificate, which is infrastructure rather than code. The RunxBuild hosting calculator shows the service, managed database, and storage as separate line items, and environment variables are configured per service in the dashboard.

Useful related references:

FAQ

What is the difference between an SSWS token and OAuth for the Okta API?

An SSWS token is a long-lived string with the full permissions of the administrator who created it. OAuth client credentials produce short-lived tokens scoped to specific operations and tied to an application rather than a person. Use OAuth in production.

How does pagination work in the Okta API?

Cursor-based, through the Link header with rel=next. There is no page number and no total count. Follow the URL in the header exactly rather than constructing it, since it contains an opaque cursor.

What are the Okta API rate limits?

They vary by endpoint and subscription level, enforced over a rolling one-minute window. Every response includes X-Rate-Limit-Remaining and X-Rate-Limit-Reset, and exceeding the limit returns a 429 with error code E0000047.

Why did my Okta API token stop working?

SSWS tokens are deactivated after 30 days of no use, and also when the administrator who created them is deactivated. The second catches integrations when someone leaves, which is a strong argument for OAuth service applications.

Should I use filter or search in the Okta API?

search for anything non-trivial, since it supports custom attributes, sorting, and more operators, at the cost of being eventually consistent. filter is strongly consistent but limited to specific indexed properties.

#Okta API#OAuth#Identity#API Tokens#SCIM