Migrating from API tokens to OAuth access tokens
Zendesk API tokens will be permanently deactivated on April 30, 2027. If your integration, script, or app currently uses an API token to call a Zendesk API, you need to migrate to OAuth before that date.
This guide shows you how to:
- Choose the right OAuth flow
- Get your first OAuth credentials
- Keep your integration working automatically when tokens expire
Disclaimer: This article is for instructional purposes only. Zendesk doesn't provide support for the example code in this article or for third-party tools such as Postman.
Why API tokens are being retired
API tokens have limitations that make them a poor fit for long-term use:
- No expiry: If a token is compromised, it stays valid until it’s manually deactivated.
- No scope restrictions: A token can access any endpoint the authenticating user is allowed to use.
- No application binding: Any system that has the token and its associated email address can use it.
Migration timeline
| Date | What changes |
|---|---|
| July 28, 2026 | New accounts can no longer create new API tokens. For all accounts, any API token that hasn’t been used for 30 days is automatically deactivated. Deactivated tokens are permanently deleted 60 days after deactivation. Existing tokens continue working unless they become inactive. |
| October 27, 2026 | All accounts can no longer create new API tokens. Existing tokens continue working unless they become inactive or are later deactivated on April 30, 2027. |
| April 30, 2027 | All remaining API tokens are permanently deactivated and can’t be reactivated. All API calls using API tokens will fail. |
What this means for your existing tokens
- Existing API tokens continue working until April 30, 2027, unless they trigger the inactivity rule first.
- The 30-day inactivity rule applies to all accounts from July 28, 2026, onward.
- Zendesk will notify you before tokens are deactivated.
- Deleted tokens can’t be reactivated. If a token is deleted before you migrate, use One-time browser setup to get new OAuth credentials.
Before you start
Before migrating, take a few minutes to:
- Make an inventory of your API tokens: In Admin Center, go to Apps and integrations > APIs > API tokens and list every active token.
- Identify what uses each token: Check your integrations, scripts, and third-party apps to find every place a token is used.
- Check for shared tokens: If multiple integrations use the same token, create a separate OAuth client for each one.
- Plan your migration: OAuth access and API tokens can run in parallel until April 30, 2027, so you can migrate one integration at a time.
How OAuth works
With API token authentication, your integration sends a static credential (your email address and API token) with every request. If that token is compromised, anyone who has it can use it until it is revoked.
OAuth works differently. Instead of a static credential, your integration goes through a one-time setup to get an access token from Zendesk. It uses that token to make API requests. The token expires after a short time, so even if it’s compromised, the exposure window is limited.
When the access token expires, your integration can automatically exchange the refresh token for a new one without any manual steps or user re-authentication.
The one-time setup varies depending on whether you want the user to grant access to your integration or not::
- If you want the user to grant access in a browser, use the authorization code flow.
- If you want your integration to run automatically with no user involved, use the client credentials flow.
Use the section below to choose the flow that applies to you.
Which OAuth flow should I use?
Choose the flow that matches how your integration runs:
| Flow | Client type | Best for |
|---|---|---|
| Authorization code | Public | Browser or mobile apps where a client secret can’t be stored securely. Requires PKCE. |
| Authorization code | Confidential | Server-side apps or scripts acting on behalf of a specific user. |
| Client credentials | Confidential | Background scripts, data pipelines, and server-to-server automation with no user interaction. |
Public clients must use the authorization code flow. Confidential clients can use either flow. The grant type is determined by the parameters you send with the request.
If you’re unsure, use the authorization code flow. It works for most integrations. Use client credentials only if your integration can’t perform the one-time browser approval step.
For more information, see Using OAuth access to authenticate API requests.
Migration overview
All integrations follow the same basic migration steps. What changes is how you get your first token and how you handle expiry.
| Step | Authorization code | Client credentials |
|---|---|---|
| 1. Set up an OAuth client | Same for both public and confidential. | Confidential clients only. |
| 2. Get your first token | Open /oauth/authorizations/new in a browser, approve access, receive a code, then exchange it for tokens through /oauth/tokens. Public clients use PKCE. Confidential clients use client_secret. See One-time browser setup. | Exchange directly for tokens through /oauth/tokens. |
| 3. Update API requests | Use the access token in the Authorization header. | Use the access token in the Authorization header. |
| 4. Handle token expiry | Exchange the refresh token for a new token pair automatically. | Request a new token using the same client credentials. |
Setting up an OAuth client
When creating your OAuth client, choose the client kind for the flow you want:
- Public clients can only use the authorization code flow.
- Confidential clients can use either flow.
Create an OAuth client in Admin Center (Apps and integrations > APIs > OAuth clients) if you haven’t already. See Registering your application with Zendesk in Zendesk help. This is required for both flows.
After you create the client, note these values:
- Identifier: Used as
client_idin API requests. - Secret: Used as
client_secretin API requests. This is shown only once after saving, so store it safely. - Redirect URLs: Used as
redirect_uri. Not needed for the client credentials flow.
After you have these values, choose your flow:
- Authorization code flow — covers both public and confidential clients. Public client steps are clearly marked where they differ.
- Client credentials flow
Authorization code grant
Use the authorization code flow if you want a user to grant access to your integration. After the initial setup, your integration can renew tokens automatically.
This section covers both public and confidential clients. Steps that differ between client types are clearly marked.
How tokens work
This flow uses two tokens:
- Access token: Used to make API requests to Zendesk. The token can last from 5 minutes up to 2 days, depending on the
expires_invalue used when it’s issued. - Refresh token: Used behind the scenes to get a new access token when the current one expires.
The lifecycle looks as follows from the perspective of your integration:
- Get an access token and a refresh token.
- Use the access token to make API requests.
- Before the access token expires, or after a request fails with
401 Unauthorized, use the refresh token to get a new access token and refresh token. - The old tokens become invalid immediately.
- The new tokens replace them.
Default token lifespans
| Token | Expires after |
|---|---|
| Access token | 30 minutes by default, configurable from 5 minutes to 48 hours |
| Refresh token | 30 days by default, configurable from 7 to 90 days |
Getting your first token
The examples below use tickets:read as a sample scope. Your integration will likely need different scopes depending on which endpoints it calls. See Scope reference for the full list.
For most migrations, explicitly request the narrowest scope your integration actually needs. This is one of OAuth’s key security advantages over API tokens.
One-time browser setup
If you’re setting up OAuth for the first time, or your API token is no longer working, you’ll need a one-time browser setup. You only do this once. After that, your integration handles token renewal automatically.
The authorization code flow works as follows:
- Send the user to Zendesk’s authorization URL.
- The user signs in and approves access.
- Zendesk redirects back with a short-lived authorization code.
- Your integration exchanges that code for an access token and refresh token.
If you use Postman for testing, see Using Postman for testing.
Step 1: Build the authorization URL
Use this template:
https://{subdomain}.zendesk.com/oauth/authorizations/new?response_type=code&client_id={your_client_id}&redirect_uri={your_redirect_uri}&scope=tickets:read
Replace {subdomain}, {your_client_id}, and {your_redirect_uri} with your values.
| Parameter | Description |
|---|---|
response_type=code | Tells Zendesk to return an authorization code |
client_id | Identifies your OAuth client |
redirect_uri | Where Zendesk sends the user after approval. Must exactly match the redirect URI configured in Admin Center |
scope | The access your token needs. See Scope reference |
state | A random string recommended for security. Verify it after Zendesk redirects back |
Public clients only — PKCE is required
If you registered your OAuth client as Public, you must generate a code_verifier and derive a code_challenge from it before building the authorization URL.
import osimport hashlibimport base64code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode()code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).rstrip(b"=").decode()
Store the code_verifier. You’ll need it in Step 3. Then add these parameters to the authorization URL:
| Parameter | Value |
|---|---|
code_challenge | The code_challenge value generated above |
code_challenge_method | S256 |
For full examples in other languages, see Using PKCE to make Zendesk OAuth access tokens more secure.
Step 2: Open the URL in a browser and approve access
The following steps walk through the authorization code flow manually, which is useful for understanding how it works and testing your OAuth client. When you’re ready to implement this in your application, see Using OAuth to authenticate Zendesk API requests in a web app and Using OAuth authentication with your application for code examples.
Paste the URL into a browser and press Enter. Zendesk asks the user to sign in, then shows an approval screen.
Click Allow. Zendesk redirects to your redirect_uri and adds a code parameter:
https://example.com/callback?code=abc123xyz
Copy the code value. It expires after 120 seconds.
If you don’t have a real callback server, set your redirect_uri to https://localhost in your OAuth client settings. After approval, the browser will try to load https://localhost and show an error. That’s expected. Copy the code from the address bar.
Step 3: Exchange the code for tokens
Now exchange the authorization code for an access token and refresh token.
All examples below use application/x-www-form-urlencoded, which is the OAuth 2.0 specification default and works with the /oauth/tokens endpoint. The endpoint also accepts application/json if your integration requires it.
Confidential clients
The following parameters identify your OAuth client:
| Parameter | Value |
|---|---|
client_id | The Identifier from your OAuth client in Admin Center |
client_secret | The Secret from your OAuth client in Admin Center |
curl request
curl -X POST https://{subdomain}.zendesk.com/oauth/tokens \-H "Content-Type: application/x-www-form-urlencoded" \--data-urlencode "grant_type=authorization_code" \--data-urlencode "code={your_authorization_code}" \--data-urlencode "client_id={your_client_id}" \--data-urlencode "client_secret={your_client_secret}" \--data-urlencode "redirect_uri={your_redirect_uri}" \--data-urlencode "scope=tickets:read"
Python request
import requestsresponse = requests.post(f"https://{subdomain}.zendesk.com/oauth/tokens",data={"grant_type": "authorization_code","code": your_authorization_code,"client_id": your_client_id,"client_secret": your_client_secret,"redirect_uri": your_redirect_uri,"scope": "tickets:read",},)response.raise_for_status()data = response.json()access_token = data["access_token"]refresh_token = data["refresh_token"]
Response
{"access_token": "gErypPlm4dOVgGRvA1ZzMH5MQ3nLo8bo","refresh_token": "31048ba4d7c601302f3173f243da835f","token_type": "bearer","scope": "tickets:read","expires_in": 1800,"refresh_token_expires_in": 2592000}
Public clients
Omit client_secret and include code_verifier instead.
| Parameter | Value |
|---|---|
code_verifier | The original random string you generated in Step 1 |
For details, see Using PKCE to make Zendesk OAuth access tokens more secure.
curl request
curl -X POST https://{subdomain}.zendesk.com/oauth/tokens \-H "Content-Type: application/x-www-form-urlencoded" \--data-urlencode "grant_type=authorization_code" \--data-urlencode "code={your_authorization_code}" \--data-urlencode "client_id={your_client_id}" \--data-urlencode "code_verifier={your_code_verifier}" \--data-urlencode "redirect_uri={your_redirect_uri}" \--data-urlencode "scope=tickets:read"
Python request
import requestsresponse = requests.post(f"https://{subdomain}.zendesk.com/oauth/tokens",data={"grant_type": "authorization_code","code": your_authorization_code,"client_id": your_client_id,"code_verifier": your_code_verifier,"redirect_uri": your_redirect_uri,"scope": "tickets:read",},)response.raise_for_status()data = response.json()access_token = data["access_token"]refresh_token = data["refresh_token"]
Response
{"access_token": "gErypPlm4dOVgGRvA1ZzMH5MQ3nLo8bo","refresh_token": "31048ba4d7c601302f3173f243da835f","token_type": "bearer","scope": "tickets:read","expires_in": 1800,"refresh_token_expires_in": 2592000}
The refresh_token in this response is the only time Zendesk issues you a refresh token for this authorization. Store it securely. You’ll use it to renew your access token automatically going forward.
The scope field shows the actual scope granted to the token. If you omit scope in the request, Zendesk defaults it to read write.
Store the access_token and refresh_token securely. See Storing tokens and client secrets securely.
This is the only time you need a browser. From here, your integration handles token renewal automatically using the refresh token.
Next: Go to Update your API requests, then set up automatic token refresh. See Implement token refresh.
Update your API requests
After you get an access token, API requests themselves change very little. Instead of sending your email address and API token, send the access token in the Authorization header.
Before (API token)
AUTH = (f"{email}/token", api_token)response = requests.get(url, auth=AUTH)
After (access token)
headers = {"Authorization": f"Bearer {access_token}"}response = requests.get(url, headers=headers)
That’s the only change needed in the request itself.
Implement token refresh
Access tokens expire after 30 minutes by default. Rather than generating a new token manually, your integration should refresh it automatically. See Working with OAuth refresh tokens.
When you need a new access token, send a refresh request using your refresh token.
curl request
curl -X POST https://{subdomain}.zendesk.com/oauth/tokens \-H "Content-Type: application/x-www-form-urlencoded" \--data-urlencode "grant_type=refresh_token" \--data-urlencode "refresh_token={your_refresh_token}" \--data-urlencode "client_id={your_client_id}" \--data-urlencode "client_secret={your_client_secret}"
Python request
def refresh_oauth_token(subdomain, client_id, client_secret, refresh_token):url = f"https://{subdomain}.zendesk.com/oauth/tokens"payload = {"grant_type": "refresh_token","refresh_token": refresh_token,"client_id": client_id,"client_secret": client_secret,}response = requests.post(url, data=payload)response.raise_for_status()data = response.json()return {"access_token": data["access_token"],"refresh_token": data["refresh_token"],"expires_in": data["expires_in"],}
Response
{"access_token": "NEW_ACCESS_TOKEN","refresh_token": "NEW_REFRESH_TOKEN","token_type": "bearer","scope": "tickets:read","expires_in": 1800,"refresh_token_expires_in": 2592000}
Save both the new access_token and refresh_token immediately. The old tokens stop working as soon as the refresh succeeds.
Add auto-refresh to your code
A simple way to handle expiry is to retry when Zendesk returns 401 Unauthorized and the error body confirms the token is invalid.
import requestsdef refresh_oauth_token(subdomain, client_id, client_secret, refresh_token):url = f"https://{subdomain}.zendesk.com/oauth/tokens"payload = {"grant_type": "refresh_token","refresh_token": refresh_token,"client_id": client_id,"client_secret": client_secret,}response = requests.post(url, data=payload)response.raise_for_status()data = response.json()return {"access_token": data["access_token"],"refresh_token": data["refresh_token"],"expires_in": data["expires_in"],}def make_request_with_refresh(url, access_token, refresh_token, client_id,client_secret, subdomain):headers = {"Authorization": f"Bearer {access_token}"}response = requests.get(url, headers=headers)if response.status_code == 401:body = {}try:body = response.json()except ValueError:passif body.get("error") == "invalid_token":new_tokens = refresh_oauth_token(subdomain, client_id, client_secret, refresh_token)headers["Authorization"] = f"Bearer {new_tokens['access_token']}"response = requests.get(url, headers=headers)return response, new_tokensreturn response, None
When an access token has expired, Zendesk returns a 401 response with this body:
{"error": "invalid_token","error_description": "The access token provided is expired, revoked, malformed or invalid for other reasons."}
For a more complete implementation that refreshes tokens before they expire, see Using OAuth to authenticate Zendesk API requests in a web app.
Client credentials grant
Use the client credentials flow for server-to-server automation and background jobs where no user is available at runtime to approve access. This flow does not issue a refresh token. When the access token expires, your integration simply requests a new one.
The client credentials flow is only suitable for secure server-side environments where your client_secret can be kept private. Never use it in a browser-based app, mobile app, or any environment where the secret could be exposed.
Getting an access token
The examples below use tickets:read as a sample scope. Your integration will likely need different scopes. See Scope reference for the full list.
curl request
curl -X POST https://{subdomain}.zendesk.com/oauth/tokens \-H "Content-Type: application/x-www-form-urlencoded" \--data-urlencode "grant_type=client_credentials" \--data-urlencode "client_id={your_client_id}" \--data-urlencode "client_secret={your_client_secret}" \--data-urlencode "scope=tickets:read"
Python request
import requestsresponse = requests.post(f"https://{subdomain}.zendesk.com/oauth/tokens",data={"grant_type": "client_credentials","client_id": your_client_id,"client_secret": your_client_secret,"scope": "tickets:read",},)response.raise_for_status()data = response.json()access_token = data["access_token"]
Response
{"access_token": "gErypPlm4dOVgGRvA1ZzMH5MQ3nLo8bo","token_type": "bearer","scope": "tickets:read","expires_in": 1800}
This response does not include a refresh_token. Store the access token securely. See Storing tokens and client secrets securely.
If you use Postman for testing, see Using Postman for testing.
Next, go to Update your API requests, then set up automatic token renewal for when the token expires. See Request a new token when it expires.
Update your API requests
Use the access token in the request header, just as you would with the authorization code flow.
Before (API token)
AUTH = (f"{email}/token", api_token)response = requests.get(url, auth=AUTH)
After (access token)
headers = {"Authorization": f"Bearer {access_token}"}response = requests.get(url, headers=headers)
Request a new token when it expires
Because the client credentials flow doesn’t issue a refresh token, your integration requests a new token by repeating the original token request.
def get_client_credentials_token(subdomain, client_id, client_secret,scope="tickets:read"):response = requests.post(f"https://{subdomain}.zendesk.com/oauth/tokens",data={"grant_type": "client_credentials","client_id": client_id,"client_secret": client_secret,"scope": scope,},)response.raise_for_status()data = response.json()return {"access_token": data["access_token"],"expires_in": data["expires_in"],}
You can call this proactively before each request or reactively after a 401 Unauthorized response.
def make_request(url, subdomain, client_id, client_secret,scope="tickets:read"):token_data = get_client_credentials_token(subdomain, client_id, client_secret, scope)headers = {"Authorization": f"Bearer {token_data['access_token']}"}return requests.get(url, headers=headers)
When an access token has expired, Zendesk returns a 401 response with this body:
{"error": "invalid_token","error_description": "The access token provided is expired, revoked, malformed or invalid for other reasons."}
Scope reference
When you request a token, the scope parameter controls which Zendesk resources the token can access and whether it can read, write, or both.
If you don’t specify a scope, the token defaults to full read and write access across all Zendesk resources. For most migrations, explicitly request the narrowest scope your integration needs. This is one of OAuth’s biggest security advantages over API tokens.
For the full list of supported scopes and resources, see Scopes in the API reference.
Choosing the right scope when migrating
API tokens have no scope restrictions. When you migrate to OAuth, take the opportunity to apply least-privilege access:
- Review which Zendesk API endpoints your integration uses.
- Check whether each requires read access (
GET) or write access (POST,PUT,DELETE). - Request a scope that covers exactly those resources and access types.
For example, if your integration only reads tickets and updates user records, use tickets:read users:read users:write instead of read write.
Setting custom token lifespans
This section is optional. The default lifespans work for most integrations. Only read on if you have a specific reason to change them, such as needing tokens to last longer between refreshes.
By default, access tokens last 30 minutes and refresh tokens last 30 days. If that doesn’t suit your integration, you can set custom values when requesting or refreshing a token.
Add expires_in and/or refresh_token_expires_in to your request in seconds:
response = requests.post(f"https://{subdomain}.zendesk.com/oauth/tokens",data={"grant_type": "refresh_token","refresh_token": refresh_token,"client_id": client_id,"client_secret": client_secret,"expires_in": 3600, # 1 hour"refresh_token_expires_in": 7776000, # 90 days},)
| Parameter | Minimum | Maximum |
|---|---|---|
expires_in (access token) | 300 seconds (5 minutes) | 172800 seconds (48 hours) |
refresh_token_expires_in | 604800 seconds (7 days) | 7776000 seconds (90 days) |
Storing tokens and client secrets securely
| Where to store credentials | Best for |
|---|---|
| Environment variables | Simple scripts and local development |
| AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault | Production integrations running on servers |
| Encrypted database fields | Applications managing tokens for multiple accounts |
Multi-tenant integrations: If your integration manages tokens for many different Zendesk accounts, use encrypted database fields to store each account’s tokens separately. Use a secrets manager for your OAuth client credentials, which are shared across all accounts.
Using Postman for testing
The following Postman setup is for testing and development only. For production integrations, implement token acquisition and refresh in your application code.
Built-in OAuth 2.0 flow
If you use Postman, you can complete the authorization flow without manually building URLs or copying authorization codes. Postman handles the browser redirect and token exchange for you.
-
In your Postman request, open the Authorization tab.
-
Set Auth Type to OAuth 2.0.
-
Click Configure New Token and fill in the following fields:
Field Value Grant Type Authorization Code Callback URL https://oauth.pstmn.io/v1/callbackAuthorization URL https://{subdomain}.zendesk.com/oauth/authorizations/newAccess Token URL https://{subdomain}.zendesk.com/oauth/tokensClient id Your client_idClient Secret Your client_secretScope tickets:reador your required scopeState Any random string -
Click Get New Access Token. Postman opens a browser window to the Zendesk authorization page.
-
Sign in and click Allow. Postman captures the authorization code and exchanges it automatically.
-
Postman shows the token response. Click Use Token to apply the access token, then save the access token and refresh token as environment variables. See Storing tokens in Postman.
Note on the callback URL: If you use https://oauth.pstmn.io/v1/callback, add that exact URL as a redirect URI in your OAuth client settings in Admin Center before running this flow. If you’d rather not, use https://localhost instead. Your browser will show an error after approval, but Postman will still capture the token successfully.
Manual token exchange
Authorization code flow
-
Create a new POST request to:
https://{subdomain}.zendesk.com/oauth/tokens -
Under Headers, add:
Content-Type: application/x-www-form-urlencoded -
Under Body, choose x-www-form-urlencoded, then add:
Key Value grant_type authorization_code code your_authorization_code client_id your_client_id client_secret your_client_secret redirect_uri your_redirect_uri scope tickets:read -
Click Send.
Client credentials flow
-
Create a new POST request to:
https://{subdomain}.zendesk.com/oauth/tokens -
Under Headers, add:
Content-Type: application/x-www-form-urlencoded -
Under Body, select x-www-form-urlencoded, then add:
Key Value grant_type client_credentials client_id your_client_id client_secret your_client_secret scope tickets:read -
Click Send.
Save the access_token and refresh_token from the response as Postman environment variables. See Storing tokens in Postman.
Pre-request script for automatic token refresh
If you use a Postman collection, you can add a pre-request script that refreshes the access token automatically before it expires.
In your collection, open the Scripts tab and add this code to the Pre-request section:
const subdomain = pm.environment.get("subdomain")const clientId = pm.environment.get("client_id")const clientSecret = pm.environment.get("client_secret")const refreshToken = pm.environment.get("refresh_token")const expiresAt = parseInt(pm.environment.get("token_expires_at") || "0")// Refresh if the token expires within the next 60 secondsif (Date.now() >= expiresAt - 60000) {pm.sendRequest({url: `https://${subdomain}.zendesk.com/oauth/tokens`,method: "POST",header: { "Content-Type": "application/x-www-form-urlencoded" },body: {mode: "urlencoded",urlencoded: [{ key: "grant_type", value: "refresh_token" },{ key: "refresh_token", value: refreshToken },{ key: "client_id", value: clientId },{ key: "client_secret", value: clientSecret }]}},(err, response) => {if (err || response.code >= 300) {console.error("Token refresh failed:", err || response.json())return}const data = response.json()pm.environment.set("access_token", data.access_token)pm.environment.set("refresh_token", data.refresh_token)pm.environment.set("token_expires_at",String(Date.now() + data.expires_in * 1000))})}
This script reads your tokens from Postman environment variables and stores the new ones after each refresh. Make sure your requests use {{access_token}} in the Authorization header as a Bearer token.
Storing tokens in Postman
Store your tokens as Postman environment variables so they’re available across your collection and can be updated automatically by the pre-request script.
Required environment variables
| Variable | Value |
|---|---|
| subdomain | Your Zendesk subdomain, for example mycompany |
| client_id | Your OAuth client id |
| client_secret | Your OAuth client secret |
| access_token | The access token from your token response |
| refresh_token | The refresh token from your token response, authorization code flow only |
| token_expires_at | Set this to Date.now() + (expires_in * 1000) |
To set the initial values:
- In the bottom-left sidebar, expand ENVIRONMENTS, then click + to create a new environment and give it a name. You can also use the No environment dropdown in the top-right corner and select Add new environment.
- Add each variable from the table above as a new row, entering the name in the Variable column and its value in the Initial value column.
- For
client_secret,access_token, andrefresh_token, change the Type column from default to secret so Postman masks the values in the UI. - Click Save.
- Select your new environment from the No environment dropdown in the top-right corner to make it active. Your requests can now reference variables like
{{access_token}}.
In your requests, set Authorization to Bearer Token and use {{access_token}} as the token value. The pre-request script will keep it current automatically.
Frequently asked questions
What about webhooks that use API tokens?
Webhook authentication using API tokens will stop working on April 30, 2027. To migrate your webhooks before that date, update the webhook authentication configuration in Admin Center to use a Bearer token instead. See Creating or cloning a webhook for instructions on updating webhook authentication settings.
If your webhook triggers an integration that itself uses an API token, that integration also needs to migrate separately. See the migration steps in this guide for details.
Can I set up OAuth through Admin Center without writing code?
Admin Center lets you create and manage OAuth clients, but generating tokens still requires code or a tool like Postman.
If you want to use Postman, see Using Postman for testing. It handles the full authorization flow for you.
Can I run OAuth and API tokens in parallel during my migration?
Yes. OAuth access tokens and API tokens both work until April 30, 2027. You can migrate integrations one at a time, testing each one before switching over. There is no need to migrate everything at once.
What if my refresh token expires or my API token has already stopped working?
Use the one-time browser setup. It only takes a few minutes and you only need to do it once. After that, as long as you keep renewing your refresh token before it expires, you won’t need to touch the browser again.
I don’t have a real callback server. Is that okay?
Yes. If you’re doing one-time setup with Postman, use Using Postman for testing. If you’re completing the process manually, set your redirect_uri to https://localhost in your OAuth client settings. After approval, your browser will show an error because nothing is listening on localhost. That’s expected. Copy the code from the address bar and use it in Step 3.
Can I make my refresh token last longer?
Yes. Set refresh_token_expires_in to 7776000 seconds (90 days) when requesting a token. Make sure your integration reliably saves the new refresh token each time it refreshes, or you’ll need to repeat the setup if a refresh token is lost.
I have several integrations using the same API token. What should I do?
Create a separate OAuth client for each integration instead of sharing one. That way, if one integration has a problem, you can revoke its access without affecting the others.
I use a third-party app from the Zendesk Marketplace. Do I need to update it?
Per developer policy, third-party apps, integrations, and bots — whether listed on the Zendesk Marketplace or not — must not use a customer’s API credentials, including API tokens or the customer’s own OAuth client, to authenticate API calls.
Apps that migrate to OAuth will continue working. Apps that don’t migrate will stop working when API tokens are deactivated.
What happens if I don’t migrate before the deadline?
On April 30, 2027, all API tokens are deactivated and can’t be reactivated. If that happens, use the one-time browser setup to set up OAuth from scratch.
My integration returns 403 Forbidden even though the token was created successfully. What’s wrong?
This usually means the token’s scope doesn’t cover the endpoint you’re calling. For example, a token with "scope": "tickets:read" can’t make POST, PUT, or DELETE requests.
Check the scope field in the token response and request a new token with a scope that includes the access type you need. See Scope reference.
What scope should I use for my integration?
Start by listing every Zendesk API endpoint your integration calls, then check whether each one is a GET (needs read) or a POST, PUT, or DELETE (needs write). Set a scope that covers exactly those resources and access types. For example, if you only read tickets and update users, use tickets:read users:read users:write instead of read write. See Scope reference for the full list.