---
name: asana-oauth-app
description: Creates or signs in to an Asana account and registers an app in the Asana developer console to obtain OAuth2 client ID and client secret — with redirect URLs, the granular-scopes-vs-Full-permissions decision, workspace distribution settings, token behaviour and a safe credential handoff. Use when asked to get Asana OAuth credentials, create an Asana app, pick Asana OAuth scopes, rotate an Asana client secret, request an Asana developer sandbox, or fix an Asana authorization error like `forbidden_scopes`, `invalid_scope`, or "This app is not available to your Asana workspace or organization". For any other vendor's developer portal, use that vendor's skill instead.
---

# Asana OAuth2 App Registration

Get a working Asana OAuth2 client — an app in the developer console, redirect URLs, a scope decision, client ID and
secret — for a platform that connects many customers' Asana workspaces.

Asana's registration form is short and the OAuth flow is textbook. Two things are not, and both cost a release if you
get them wrong. First, **scopes**: Asana spent 2025 moving from a single full-access grant to per-resource granular
scopes, and the two modes are **mutually exclusive** — an app registered for granular scopes that sends the old value
fails, and an app registered for Full permissions that sends specific scopes also fails. Worse, granular scopes still
do not cover every endpoint, so "use least privilege" is not always an available answer (§5). Second, **distribution**:
a newly created app is visible to *no* workspace, so authorization fails for everyone, including you, until you set it
(§7). Neither failure looks like a scope or distribution problem from the outside.

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **App name**, icon, description | Shown on the consent screen and in the admin console |
| **Asana account** that will own the app | Any Asana user; note whose account it is (§2) |
| **Redirect URLs** | Every callback host your platform serves (§4) |
| **Which endpoints the connector actually calls** | Decides granular vs Full permissions (§5) |
| **Company URL, support URL, privacy policy URL** | Required for the app listing; asked for at App Directory submission (§7) |
| **Is an App Directory listing wanted?** | Optional — not required to serve customers (§7) |
| **Do customers need a sandbox / is one needed for testing?** | Request lead time is up to a week (§2) |

## Quick Start

1. Confirm a **new app** is needed — existing connections are bound to the current client ID (§1).
2. Sign in to Asana and open the developer console; request a sandbox if you need a safe domain (§2).
3. **Create new app** — name, redirect URL, permission scopes (§3).
4. Add **every** redirect URL, exactly (§4).
5. Make the scope decision: granular scopes, or the Full permissions toggle — not both (§5).
6. Capture client ID and secret from the **OAuth** tab; note the one-hour access token (§6).
7. Set **Manage distribution** — without it nobody can authorize; then consider admin approval and limits (§7).
8. Verify from a **second** account in a different workspace, not the one that owns the app (§8).
9. Hand the credentials over — never commit them (§9).

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **Granular OAuth scopes exist and are the default path for new apps.** Announced **9 April 2025** as a developer
  preview with a redesigned consent screen; before that, an Asana app requested full access to the user's account and
  there was no scope selection at all. At registration you now pick the scopes your app may ever request, in the
  developer console under **OAuth → Permission scopes**.
- **Legacy full access still works, as an explicit opt-out.** The console keeps a **Full permissions** toggle, and the
  corresponding authorize-time value is the literal scope string `default`. Asana's own OAuth guide still documents
  both, unchanged as of this check, and gives the reason plainly: *"scopes are not yet available for every Asana API
  endpoint."* No removal date has been announced. Treat it as supported-but-on-notice, not as a permanent answer.
- **Existing apps were not force-migrated.** The April 2025 note said existing apps "continue using full permissions
  until these self-serve granular scopes are made available later"; Asana's **21 August 2025** release notes then
  shipped "granular API scopes for existing apps … without requiring app recreation". So an older app is almost
  certainly still on full access, and moving it to granular scopes is a deliberate action someone has to take.
- **A new app gets whatever you pick at registration, and the choice is enforced at both ends.** Registered for
  granular scopes, the app *must* send an explicit `scope` parameter (this used to be optional). Registered for Full
  permissions, it must send `scope=default` or no `scope` at all. Mixing them returns `forbidden_scopes` (§5).
- **PKCE is supported and recommended, not required.** Plain authorization-code exchange with a client secret still
  works. Implicit grant (`response_type=token`) was removed back in January 2020.
- **The scope catalogue is explicitly "subject to revision"** and has real gaps — sections and team creation have no
  scope at all today (§5). The scopes reference page was last updated 2026-08-14, so it is moving.

If the developer console does not look like this, stop and report what you actually see rather than clicking on.

## 1. Decide: reuse the existing app, or register a new one

A new app means a **new client ID, and every existing customer connection is bound to the old one** — every customer
re-authorizes. Reuse the existing app for: adding a redirect URL, changing the scope set, resetting a leaked secret,
widening distribution, or diagnosing an authorization failure.

Register a **new** app only when the user explicitly wants one: a replacement for a compromised app, a separate app
for a different product, or a deliberate split of environments. Note that even on the *same* app, changing the scope
set changes what users must consent to — a token only carries the scopes granted when it was issued, so broadening
scopes is a re-authorization event for every customer. Say which path you are taking before you touch anything.

## 2. Account: sign in or sign up

- The developer console is at **`https://app.asana.com/0/my-apps`** (also reachable from your profile settings). Any
  Asana user can create apps and personal access tokens there; no separate developer account, no approval gate.
- The app is owned by the **individual Asana user** who creates it, not by a company. For a platform integration, use
  a shared or service-owned account the team controls — not a person who might leave. Ask who owns it and record it.
- **Developer sandbox** — a temporary Asana domain with limited users, requested through a form, granted only to
  developers building a third-party integration intended for the App Directory or to existing Starter-or-higher
  customers. It can include Enterprise/Advanced/Starter features on request, is valid for at most one year, and
  **takes up to a week to provision**. If testing needs a sandbox, start the request before anything else.

Hand control back to the user for anything only they can do: signup email verification, 2FA, accepting terms, or
submitting the sandbox form. Do not retry a blocked step in a loop.

**If this session has no browser automation** (the usual case for a CLI or cloud run), do not pretend to click. Hand
the user an exact, ordered click path with the literal values to paste (§4 redirect URLs, §5 scope list), then
continue once they report back with the client ID.

## 3. Create the app

In the developer console: **Create new app**. Three fields carry the weight:

1. **App name** — what users see on the consent screen and in their authorized-apps list. Editable later under
   **Basic information**.
2. **Redirect URL** — see §4.
3. **Permission scopes** — the pre-approved list of scopes the app may ever request, or the Full permissions toggle.
   See §5. Editable later under **OAuth → Permission scopes**.

Icon, description and other basics can be added at creation or later. The sidebar tabs worth knowing:

| Tab | Holds |
| --- | --- |
| **Basic information** | App name, icon, description |
| **OAuth** | **Client ID and client secret**, redirect URLs, **Permission scopes**, secret **Reset** |
| **Manage distribution** | Which workspaces may authorize the app — nobody, until you set it (§7) |
| **App listing details** | Company/support/privacy URLs shown to users and to admins approving the app |
| **App components** | In-product UI surfaces; triggers a heavier review if you publish (§7) |

Asana's docs are internally inconsistent about where the client secret lives (one table says *Basic information*).
The registration guide and the console agree it is the **OAuth** tab; trust that.

## 4. Redirect URLs

Register **every** callback host your platform serves. For Unified.to these are one per data center; confirm the
current list with the platform owner rather than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

Notes:

- **Multiple redirect URLs are supported.** This is a change from the long-standing one-URL-per-app limit that older
  forum threads and runbooks still describe; Asana's own MCP setup guide now instructs developers to register two.
  If the console shows only a single field, stop and report that rather than registering four separate apps.
- The `redirect_uri` you send must match a registered value **exactly** — no path or trailing-slash forgiveness.
  Asana's own docs warn that a copied value can lose its trailing slash.
- **HTTPS is enforced** for non-native apps; Asana refuses plaintext redirect endpoints because the code travels in
  the query string. Native/CLI apps use the special value `urn:ietf:wg:oauth:2.0:oob`.
- A mismatched `redirect_uri` — like a mismatched `client_id` — is one of the few errors shown as **plain text on
  Asana's page** instead of being redirected back to you, because Asana has nowhere it trusts to send it. Every other
  error comes back on the redirect URI.

## 5. Scopes — the decision that defines the app

Scopes are space-delimited in the authorize URL and **must be URL-encoded** (spaces become `%20`). The format is
`<resource>:<action>` where action is `read`, `write` or `delete`, and **they do not imply each other**: `tasks:write`
does not grant `tasks:read`. Request both, always, for anything you both read and modify.

The scopes a task/project connector typically needs:

| Scope | For |
| --- | --- |
| `tasks:read` `tasks:write` `tasks:delete` | Tasks, subtasks, dependencies, project/tag membership, search |
| `projects:read` `projects:write` `projects:delete` | Projects, per-team and per-workspace project lists, custom field settings |
| `stories:read` `stories:write` | Comments and activity on tasks and goals |
| `attachments:read` `attachments:write` `attachments:delete` | Files on tasks |
| `users:read` | Users, workspace and team membership lists, and *any* nested user field (see below) |
| `teams:read` `team_memberships:read` | Teams and their members |
| `workspaces:read` | The workspace list — how you discover what the token can see |
| `custom_fields:read` `custom_fields:write` | Custom field definitions and enum options |
| `tags:read` `tags:write` | Tags |
| `webhooks:read` `webhooks:write` `webhooks:delete` | Webhook subscriptions — easy to forget until sync silently stops |
| `openid` `email` `profile` | Identity only, via OpenID Connect. Not data access |

Others exist (portfolios, goals, roles, time tracking, out-of-office, project/task templates, custom types, jobs,
`workspaces.typeahead:read`). Read the scopes reference rather than guessing a name — a name that does not exist is
rejected as `invalid_scope`.

**Three rules that bite:**

1. **Full permissions and granular scopes are mutually exclusive.** With Full permissions on, sending specific scopes
   fails with `forbidden_scopes: Your app is not allowed to request user authorization for '<scope>' scopes.` — send
   no `scope` parameter, or `scope=default`. With granular scopes registered, sending `default` fails with
   `forbidden_scopes: … for default identity scopes`. Requesting a scope you never registered fails the same way.
2. **You may request a subset of what you registered, but requests are never additive.** To add a scope later, send
   the user back through authorization with the **complete** list — the new token replaces the old grant. Check what
   a live token actually carries with the token introspection endpoint rather than assuming.
3. **Nested objects are redacted unless you hold their scope.** On any related object in a response, only `gid`,
   `name`/`title`, `resource_type` and `resource_subtype` come back by default. Anything more, requested through
   `opt_fields`, needs that object's scope: `opt_fields=assignee.email` without `users:read` is a **403**, not a
   quietly missing field. A connector that maps assignee emails, follower names or project owners needs `users:read`
   even though it never calls a user endpoint.

**Where granular scopes genuinely cannot reach.** As of this check the catalogue has no scope for **sections**
(`GET`/`POST /projects/{gid}/sections`, `GET /sections/{gid}`) and **no `teams:write`** — creating a team has no
granular scope. A connector that manages project sections or creates teams therefore cannot run on granular scopes
today; Full permissions is the only working configuration for it. This is also not hypothetical drift: in December
2025 the API demanded a `custom_field_settings:read` scope that the console did not offer, and Asana's own advice was
to fall back to `scope=default` until it was fixed (by inheriting from the parent resource) in February 2026. Expect
the gap list to move; re-derive it from the scopes reference for the endpoints your connector actually calls.

> **As of 2026-09-20, Unified.to's Asana connector requests the legacy full-access permission value — the literal
> `default` scope — for every object it supports** (tasks, projects, comments, files, people/teams and custom field
> metadata, reads and writes alike), plus `openid`, `email` and `profile` for the login flow. It does **not** request
> granular per-resource scopes, and it does **not** use PKCE for Asana. In practice that means the app it is pointed
> at **must** have the Full permissions toggle on: register granular scopes instead and every authorization fails at
> the consent screen with `forbidden_scopes`. It also means the connector inherits whatever deprecation timeline
> Asana eventually puts on full access, and that its stored notes about which scopes "don't exist" reflect the 2024
> catalogue, not today's. Confirm the current state with the connector's owner before registering anything.

Say in your summary which mode you chose and why. If you choose Full permissions, say explicitly that it is a
deliberate fallback because of the endpoint gaps — customer security reviewers ask, and "we didn't look" is a bad
answer.

## 6. Capture the credentials

From the developer console → your app → **OAuth** tab: **client ID** and **client secret**. The secret can be re-read
there, and **Reset** issues a new one.

Capture and record:

- **Client ID** and **client secret**
- Authorize: `GET https://app.asana.com/-/oauth_authorize`
- Token exchange and refresh: `POST https://app.asana.com/-/oauth_token`
- Revoke: `POST https://app.asana.com/-/oauth_revoke` · Introspect: `POST https://app.asana.com/-/token_info`
- API base: `https://app.asana.com/api/1.0/` — one host for everyone, no per-customer domain to discover
- OIDC user info: `https://app.asana.com/api/1.0/openid_connect/userinfo` · discovery:
  `https://app.asana.com/api/1.0/.well-known/openid-configuration`
- Whether the app is registered for Full permissions or a granular scope list, and the exact scope strings

Authorize parameters: `client_id`, `redirect_uri`, `response_type`, **`state` (documented as required)**, `scope`,
plus `code_challenge` / `code_challenge_method=S256` if you use PKCE. `response_type` is `code`, `id_token`, or the
space-delimited `code id_token` when you want an ID token alongside the code.

**Token behaviour:**

- **Access tokens last one hour** (`expires_in: 3600`). Short — a connector must refresh, not re-authorize.
- **Refresh tokens are long-lived and do not rotate.** They are returned only on the initial code exchange; a refresh
  response does not hand back a new one, so keep using the original. Introspection reports a refresh token's
  remaining life in the order of years. There is no documented idle expiry, which makes a sudden `invalid_grant` a
  revocation or a scope change, not a timeout.
- Both the exchange and the refresh are **form-encoded POSTs carrying `client_id` and `client_secret`** — this is a
  confidential client; there is no public-client variant to fall back on.
- The exchange response includes a `data` object with the authorizing user's `gid`, `name` and `email` — enough to
  identify the connection without a separate call.
- **Revocation takes the refresh token, not an access token** (bearer tokens are rejected outright). Revoking the
  refresh token kills the bearer tokens derived from it.
- **Treat tokens as opaque.** Asana explicitly warns that token formats may change without notice — any client-side
  validation of their shape is a future outage.

**The PAT alternative, and why it is not the answer here.** The same console issues personal access tokens: long-
lived, no OAuth flow, and carrying exactly the access of the user who created them. They are fine for a script or a
one-off backfill. They are not a multi-tenant answer — a PAT is one person's credential in one person's workspaces,
every action is attributed to them, it dies when they leave or lose access, and you would be asking each customer to
generate and paste a password-equivalent. Asana's own guidance is that an app acting on behalf of users should use
OAuth. If someone proposes PATs to skip registration, say this plainly.

Resetting the client secret invalidates the old one: existing access tokens live out their hour, but every exchange
and refresh fails until the new secret is deployed. Never reset without explicit go-ahead and a cutover plan.

Report the secret once so the user can paste it into their secret store, say plainly that it is now in the transcript
and can be reset, then move on.

## 7. Distribution, admin control, and limits

**Distribution is not optional.** A newly created app is visible to no workspace, and the authorize endpoint refuses
everyone — including the app's own creator — with *"This app is not available to your Asana workspace or
organization."* In **Manage distribution**, choose:

- **Specific workspaces** — one or more workspaces *you are a member of*. Any member of those workspaces can then
  authorize. Selecting the mode but adding no workspace leaves the app broken in exactly the same way as not setting
  it, which is a common self-inflicted wound.
- **Any workspace** — any Asana user with the authorize URL can connect. This is what a multi-tenant connector needs,
  and it is **self-serve: no review, no approval, no listing required**.

Narrowing from Any workspace back to Specific blocks *new* authorizations only; existing tokens keep working.

**Workspaces and organizations.** A workspace is the top-level container; an *organization* is a workspace tied to a
company's email domains, and you tell them apart by the `is_organization` property. Users can belong to several, and
the token sees exactly what its user sees — Asana has no tenant-admin grant in the OAuth flow. Discover reachable
workspaces with `GET /workspaces` rather than asking the customer. Asana announced in April 2023 that authorizations
would become **workspace-scoped**, so a user in several workspaces authorizes once per workspace; current docs do not
restate this, so confirm at the consent screen during §8 rather than assuming either behaviour.

**Admins can block you.** Organization-level super admins have an **Apps** tab in the admin console (not available to
divisions) and can run an approve-list mode in which users cannot connect anything unapproved — they see a "request
admin approval" prompt instead. Your **App listing details** (company, support URL, privacy policy) are what that
admin reads when deciding. Fill them in even if you never publish. A connector that works at one customer and is
refused at another, with no error from Asana's API, is usually this.

**Service Accounts are the admin-installed alternative, and are Enterprise-only.** A super admin creates one; it has
complete access to all data in the organization, including private user data, and it authenticates with its own
token rather than through your OAuth app. Its scopes are a different system (SCIM user provisioning, audit logs,
exporting, workspace events, AI Studio usage, plus a Full Permissions option for the standard API). Recommend it only
when a customer explicitly wants org-wide, non-user-attributed access — and say out loud that it is a broader grant
than an OAuth connection, not a narrower one.

**Rate limits are per token, not per app or per workspace:**

| Limit | Value |
| --- | --- |
| Requests/minute | **150** on free domains, **1,500** on paid |
| Concurrent requests | 50 GET, 15 POST/PUT/PATCH/DELETE (reads and writes limited independently) |
| Search API | 60 requests/minute, separate from the above |
| Duplication / instantiation / export jobs | 5 concurrent per user, shared with jobs started in the web UI |
| Cost-based limiter | Expensive responses (many `opt_fields`, deep graph traversal) are costed **after** the response is built and deducted from a per-minute quota — you can be throttled at low request volume |

`429` responses carry `Retry-After` in seconds, and **rejected requests still count against the quota**, so an
impatient retry loop extends the outage. The per-token shape means one noisy customer cannot throttle another, but a
connector that fans out across a customer's whole workspace on one token can throttle itself.

**App Directory listing is optional.** Submitting for review is not required to obtain a token or to serve customers;
it is for discoverability. The app must be shared to Any workspace, the listing fields must be filled, and Asana's
team aims for an **initial response within a week** — longer for apps with app components, which get a security and
QA review of their endpoints and authorization. Do not promise a customer a listing date.

## 8. Verify end-to-end

Authorizing in your own workspace with the app distributed to that workspace proves almost nothing. Test the path a
customer takes:

1. Authorize as a **different user in a different workspace**, through your platform's real connect flow, with the
   distribution setting you intend to ship.
2. Read the consent screen: confirm the permissions listed match the scope mode you registered, and note whether it
   asks the user to pick a workspace.
3. Confirm the token response carries a **refresh token** and the expected `data` user block, then force a **refresh**
   and confirm the same refresh token still works afterwards.
4. Call one endpoint per object the connector maps — including one with `opt_fields` that reaches into a nested
   object — so a missing `users:read` surfaces now and not in production.
5. If webhooks are in play, create one. Webhook scopes are separate and are the classic omission.

| Symptom | Cause |
| --- | --- |
| Plain-text error page, never redirected back | `client_id` or `redirect_uri` does not match a registered value (§4) |
| "This app is not available to your Asana workspace or organization" | Distribution not set, or set to Specific with no workspace added (§7) |
| `forbidden_scopes: … for default identity scopes` | App is registered for granular scopes but the client sends `default` (§5) |
| `forbidden_scopes: … for '<scope>' scopes` | App is on Full permissions but the client sends specific scopes — or the scope was never registered (§5) |
| `invalid_scope` | Scope name does not exist in Asana's catalogue, or the API wants a scope the console does not offer (§5) |
| 403 on a field that exists in the docs | Nested field requested via `opt_fields` without that object's scope (§5) |
| 403 only at one customer, no API error visible | Admin app-approval mode in their organization (§7) |
| Auth works, writes fail | `write` does not imply `read` and vice versa — register both (§5) |
| Everything 401s after an hour | Client is not refreshing; access tokens live 3600s (§6) |
| `invalid_grant` on refresh | Token revoked, app disconnected by the user, or scopes changed — re-authorize (§6) |
| 429 at low request volume | Cost-based limiter on expensive `opt_fields` responses (§7) |

## 9. Hand off — never commit the secret

- **Do not** write the client secret into source control, a test, a fixture, a committed `.env`, a ticket, a PR body,
  or a chat channel. Values go to the user, for the secret store or console.
- If a code change is needed (a redirect host, a scope list), keep it secret-free and say what the human must set out
  of band.
- Close with: app name and the Asana account that owns it; client ID; where the secret was delivered; the authorize,
  token, revoke and API base URLs; **the scope mode (Full permissions vs granular) and the exact scope strings**;
  the distribution setting; whether a listing or sandbox was requested; and anything left for the user to do.

## Stop and ask

Hand back to a human rather than guessing when: the endpoints the connector needs have no granular scope and someone
must accept Full permissions as a deliberate choice; moving an existing app from full access to granular scopes is
proposed (it re-authorizes every customer); a customer's admin has blocked or must approve the app; someone proposes
a Service Account or personal access tokens in place of OAuth (both are broader or single-user grants); an App
Directory submission asks for compliance, legal, security-review or volume claims; a sandbox request form asks for
company details you were not given; or the developer console does not match the **Platform state** section above.

## References

Official Asana docs only; every URL below returned HTTP 200 on 2026-09-20. The developer console itself lives at
`https://app.asana.com/0/my-apps` and requires sign-in.

- OAuth (registration, endpoints, scopes, PKCE, troubleshooting) — https://developers.asana.com/docs/oauth
- OAuth scopes reference (the full catalogue and its endpoint map) — https://developers.asana.com/docs/oauth-scopes
- Authentication overview (OAuth vs PAT vs Service Accounts) — https://developers.asana.com/docs/authentication
- Personal access tokens — https://developers.asana.com/docs/personal-access-token
- OpenID Connect — https://developers.asana.com/docs/openid-connect
- Share your app / distribution and listing details — https://developers.asana.com/docs/share-your-app
- Publish your app (App Directory review) — https://developers.asana.com/docs/publish-your-app
- App listing guidelines — https://developers.asana.com/docs/app-listing-guidelines
- Rate limits — https://developers.asana.com/docs/rate-limits
- Input/output options (`opt_fields`) — https://developers.asana.com/docs/inputoutput-options
- Workspaces and organizations — https://developers.asana.com/docs/workspaces
- Developer sandbox — https://developers.asana.com/docs/developer-sandbox
- Deprecations process and `Asana-Change` headers — https://developers.asana.com/docs/deprecations
- OAuth client libraries and Postman collection — https://developers.asana.com/docs/getting-started-with-asana-oauth
- Changelog: OAuth permission scopes (9 April 2025) — https://forum.asana.com/t/new-oauth-permission-scopes/1048556
- Admin app management and approval modes — https://help.asana.com/s/article/app-management-and-integrations?language=en_US
- Service accounts — https://help.asana.com/s/article/service-accounts?language=en_US
- App Directory — https://asana.com/apps
