Nifty API
  • Authentication
  • OAuth 2.1
  • v1.0 API
  • API tokens
Getting started

Authorization (OAuth 2.1)

Authorization (OAuth 2.1)

OAuth 2.1 is how a third-party app asks a Nifty team for permission to call the API on its behalf, without ever handling the user's password. If you are only scripting against your own account, use a Personal Access Token instead — this page is for integrations you ship to other people's teams.

Nifty's authorization server implements the authorization-code grant with PKCE, which OAuth 2.1 requires of every client. The implicit and resource-owner-password grants are not supported.

Scope requirement for the v1.0 REST API. The v1.0 surface only accepts a token carrying the admin ("Full access") scope tier, and the OAuth 2.1 authorization server does not issue admin — so an OAuth 2.1 access token is rejected with 403 Forbidden on /api/v1.0/*. Existing legacy Nifty App integrations are unaffected and continue to work.

Endpoints

PurposeURL
Discovery (RFC 8414)https://api.niftypm.com/.well-known/oauth-authorization-server
Authorizationhttps://api.niftypm.com/oauth/authorize
Tokenhttps://api.niftypm.com/oauth/token

Read the discovery document rather than hard-coding those URLs. It is the authoritative description of this authorization server, and it is what tells you — without a support ticket — which grant types, scopes, PKCE methods and client-authentication methods are actually accepted right now. Most OAuth libraries can consume it directly; point yours at the issuer https://api.niftypm.com and let it resolve the rest.

TerminalCode
curl https://api.niftypm.com/.well-known/oauth-authorization-server

The authorization server runs per environment. Substitute the host if you are building against a non-production environment — QA is https://awsapi.niftyqa.com and UAT is https://awsapi.niftyuat.com; each serves its own discovery document at the same path.

Registering your app

Nifty does not support dynamic client registration (RFC 7591) — there is no registration_endpoint, and one is not advertised in the discovery document. Register your app by hand: in Nifty, go to Settings → App Center → "Integrate with API".

Registration gives you a client_id, and — if you register a confidential client — a client_secret that is displayed once, at creation time. Store it then; it is never shown again.

Client typeCredentialstoken_endpoint_auth_method
Public — mobile, desktop, single-page appsclient_id onlynone
Confidential — server-side apps that can keep a secretclient_id + client_secretclient_secret_post

Client credentials are sent in the token request body. HTTP Basic (client_secret_basic) is not among the advertised authentication methods.

Redirect URI rules

You must register at least one redirect URI, and you may register several.

  • Matching is an exact, full-string comparison. The redirect_uri you send to the authorization endpoint must be byte-identical to one of the registered values — including scheme, host, port, path, and any trailing slash.
  • There are no wildcards and no prefix matching. A literal * is rejected at registration time. If your app needs several callback addresses, register each one in full.
  • A redirect_uri that does not match a registered value is rejected with invalid_request before any authorization code exists.

The authorization code flow

1. Create a PKCE code verifier and challenge

The code_verifier is a high-entropy random string you keep private for the duration of the flow. The code_challenge is its SHA-256 hash, base64url encoded — S256 is the only challenge method this server accepts.

TerminalCode
code_verifier=$(openssl rand -base64 96 | tr -d '\n=' | tr '+/' '-_' | cut -c1-128) code_challenge=$(printf '%s' "$code_verifier" | openssl dgst -binary -sha256 | openssl base64 | tr -d '\n=' | tr '+/' '-_')

Keep code_verifier in the user's session — you need it again in step 4, and it must never travel through the browser redirect.

2. Send the user to the authorization endpoint

Redirect the user's browser (not a background HTTP request) to the authorization endpoint. Nifty responds with a redirect to its consent screen, where the user picks the workspace and approves the access you asked for.

Code
https://api.niftypm.com/oauth/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback &scope=read%20write &state=RANDOM_OPAQUE_VALUE &code_challenge=YOUR_CODE_CHALLENGE &code_challenge_method=S256
ParameterRequiredNotes
response_typeyescode — the only supported response type
client_idyesFrom registration
redirect_uriyesMust exactly match a registered value
scopeyesSpace-delimited (see Scopes)
stateyesOpaque anti-CSRF value; verify it on the way back
code_challengeyesFrom step 1
code_challenge_methodyesS256 — the only accepted method

3. Receive the authorization code

When the user approves, their browser is redirected back to your registered redirect_uri with the code in the query string (query is the only supported response mode):

Code
https://app.example.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_OPAQUE_VALUE&iss=https%3A%2F%2Fapi.niftypm.com

Before doing anything else:

  • Check state matches the value you sent.
  • Check iss equals https://api.niftypm.com. This server sets authorization_response_iss_parameter_supported, so the issuer is always returned and validating it defends against mix-up attacks.

If the user declines, you get ?error=access_denied&state=… instead.

The authorization code expires after 60 seconds and can be redeemed exactly once. Exchange it immediately; a second exchange of the same code fails.

4. Exchange the code for tokens

POST to the token endpoint as application/x-www-form-urlencoded.

TerminalCode
curl -X POST https://api.niftypm.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=authorization_code" \ --data-urlencode "code=$AUTHORIZATION_CODE" \ --data-urlencode "code_verifier=$code_verifier" \ --data-urlencode "redirect_uri=https://app.example.com/callback" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET"

Omit client_secret for a public client; send it for a confidential one. code_verifier is required for every client, public or confidential.

Code
{ "access_token": "nft_oauth_1a2b3c4d_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_a1b2", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "<opaque refresh token>", "scope": "read write" }

The scope echoed here is the coarse summary of what was granted; the token itself carries and enforces the fully expanded fine-grained set.

Use the access token like any bearer credential:

Code
Authorization: Bearer nft_oauth_<...>

Token lifetimes and refreshing

TokenLifetime
Authorization code60 seconds, single use
Access token3600 seconds (1 hour)
Refresh token7 days

Always trust expires_in on the token response over a hard-coded constant.

To mint a fresh pair before the access token expires, use the refresh_token grant:

TerminalCode
curl -X POST https://api.niftypm.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=refresh_token" \ --data-urlencode "refresh_token=$REFRESH_TOKEN" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET"

The response has the same shape as step 4 — including a new refresh_token.

Three things to get right:

  • Refresh tokens rotate. Every successful refresh returns a new refresh token and consumes the old one. Persist the new value immediately, replacing the old one.
  • Replay revokes everything. Presenting a refresh token that has already been used is treated as a compromise: the entire token family is revoked and the user has to authorize your app again. This is why storing the newest token — and never retrying a refresh with a stale one — matters.
  • Scope is re-clamped on every refresh against the granting member's current role.

Public clients refresh with client_id alone; confidential clients must still present client_secret.

Scopes

Nifty advertises its scopes in scopes_supported, on two levels.

Coarse axes — read, write, delete. Request these when your app wants broad access and you would rather not enumerate resources. The authorization server expands them for you:

AxisExpands to
readevery <resource>:read
writeevery <resource>:write, <resource>:read and <resource>:create
deleteevery <resource>:delete

Fine-grained scopes — <resource>:<action>, where the resource is plural and the action is one of read, write, create or delete. For example tasks:read, chats:write, documents:create, projects:delete. Request these when you want least privilege. The full list is in scopes_supported in the discovery document.

Scopes are space-delimited in both the authorization request and the token response.

Scope is clamped to the granting member's role

Whatever you register and whatever you request is a ceiling, not a guarantee. At consent time, and again every time a token is issued or refreshed, the requested scope is intersected with the permissions the approving member's role actually grants. A member who cannot delete projects cannot grant your app projects:delete, however you asked for it.

Practical consequences:

  • Always read the scope field on the token response; it is the authoritative record of what you got.
  • If the intersection is empty, authorization fails with invalid_scope.
  • Because the clamp is re-applied on refresh, a token's effective access can shrink over the life of a connection.

There is no admin scope obtainable through OAuth 2.1. It is not advertised in scopes_supported, and it is filtered out before coarse-axis expansion, so it cannot be requested directly or acquired indirectly.

Migrating from OAuth 2.0

If you have implemented an OAuth 2.0 client before, these are the differences that will actually break your code:

  • PKCE is mandatory for every client — not just public ones. A confidential client with a client_secret must still generate a code_verifier and send it on the token exchange. S256 is the only accepted challenge method; plain is not supported.
  • The implicit grant is gone. response_type=token is not supported; code is the only response type. Tokens are never returned in a redirect.
  • The resource-owner password grant is gone. Only authorization_code and refresh_token are supported. Never ask a user for their Nifty password.
  • Redirect URIs are matched exactly. Any wildcard or prefix matching your OAuth 2.0 client relied on will fail.
  • Refresh tokens are single-use and rotate. A client that stores the original refresh token and reuses it will get its whole token family revoked.
  • Client credentials go in the request body (client_secret_post), not in an HTTP Basic header.
  • Validate the iss parameter on the authorization response.

Error responses

Errors follow RFC 6749: a JSON body of { "error": …, "error_description": … } from the token endpoint, and ?error=…&state=… on the redirect back from the authorization endpoint.

errorUsually means
invalid_requestA required parameter is missing or malformed — including a redirect_uri that does not exactly match a registered value
invalid_clientUnknown client_id, or a confidential client sent a missing or wrong client_secret
invalid_grantThe code or refresh token is expired, already used, or does not belong to this client; also a failed code_verifier check
invalid_scopeThe requested scope is not advertised, or the approving member's role grants none of it
unsupported_response_typeAnything other than response_type=code
access_deniedThe user declined at the consent screen
temporarily_unavailableThe authorization server is disabled for this environment; retry later
Last modified on August 14, 2026
On this page
  • Endpoints
  • Registering your app
    • Redirect URI rules
  • The authorization code flow
    • 1. Create a PKCE code verifier and challenge
    • 2. Send the user to the authorization endpoint
    • 3. Receive the authorization code
    • 4. Exchange the code for tokens
  • Token lifetimes and refreshing
  • Scopes
    • Scope is clamped to the granting member's role
  • Migrating from OAuth 2.0
  • Error responses
JSON