Skip to main content
Version: 2026-05

OAuth

OAuth lets your app call the &Open API in a customer's account without the customer handing over an API key. Use it when you're building an app that many &Open customers install, such as a CRM integration. The customer signs in to &Open, approves what your app may do, and your app receives an access token for that account.

If you're connecting a single &Open account to your own systems, use an API key instead. See Authentication.

How OAuth connections work​

&Open uses the OAuth 2.0 authorization-code flow with PKCE (Proof Key for Code Exchange). Access tokens are standard bearer tokens, and the API publishes the metadata a standard OAuth client library needs to find everything else.

One registered app serves every &Open customer. The account a connection reaches depends on where the person signs in, so there's nothing to configure per customer. You can't choose which person or which &Open account a token acts for: there's no parameter for either, which keeps one customer's data out of reach of another's.

User-level and account-level grants​

&Open registers each app for one kind of grant, and the app can't switch between them later.

A user-level grant acts as the person who connected. The token sees what that person sees in &Open: an admin sees every gift request in the account, and anyone else only the gift requests they sent. Gift requests it creates are sent by that person. The connection ends when the person loses their &Open login, for example when they leave their company.

If several people in the same workspace in your app want to send gifts as themselves, each one connects separately. A person with logins to two &Open accounts connects twice, once for each.

An account-level grant acts for the whole account, and keeps working after the person who authorized it leaves. That reach is why only an admin can authorize it; anyone else gets access_denied with admin_consent_required. The token sees every gift request in the account and can cancel any of them. A gift request it creates is sent by the campaign's owner, as one created with an API key is.

Before you start​

&Open registers your app for you. Dynamic client registration (POST /oauth/register) creates a different kind of client that can't request the gifting:write scope, so don't use it.

To register, contact us with:

  • the redirect URI for each environment. Each must use https, and we match it exactly, character for character
  • confirmation that your app keeps its client secret on your server
  • a technical contact for credentials and change notices

We send you a client_id and client secret for each environment, along with the host names to use. We show the secret once, so store it somewhere safe straight away.

The customer's account must be on the Enterprise tier and switched on for OAuth, and for your app's kind of grant. This applies to both kinds. If someone from an account that isn't switched on tries to connect, the flow ends with access_denied and account_not_entitled. Signing in as a different person doesn't change this. See Errors and what to do.

Regions and the resource identifier​

Each region has its own authorization server and its own resource identifier: the region's API base URL with the API version appended.

RegionResource identifier
US / Globalhttps://api.andopen.co/2026-05
EUhttps://api.eu.andopen.co/2026-05

A token from one region is rejected by the other with 401 unauthorized, so an app that serves accounts in both regions runs the flow against each. For staging, read the values from the metadata document described in Discover the authorization server. The examples below use the US hosts.

The flow​

  1. Your app sends the person to the &Open authorize endpoint.
  2. They sign in to their &Open account and see a consent screen with your app's name and a plain-language summary of what it can do.
  3. When they choose Allow, &Open redirects to your redirect URI with a code and your original state.
  4. Your server exchanges the code for an access token and a refresh token.
  5. Your server calls the API with the access token and refreshes it when it expires.

Discover the authorization server​

Read the resource identifier, the authorization server, and the scope from the API's metadata document rather than hard-coding them per region. The table in Regions and the resource identifier is for reference. Every 401 from the API carries a WWW-Authenticate header that points at the API's metadata document:

Response header · 401
WWW-Authenticate: Bearer resource_metadata="https://api.andopen.co/.well-known/oauth-protected-resource/2026-05"

A request with no Authorization header, or with a scheme other than Bearer, gets that pointer alone. When you sent a token and the API rejected it, the challenge also carries error="invalid_token", whatever the reason:

Response header · 401, token rejected
WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://api.andopen.co/.well-known/oauth-protected-resource/2026-05"

The metadata document is public. It names the resource identifier, the authorization server, and the scope to ask for; see Get the protected-resource metadata. The authorization server publishes its own endpoints in its RFC 8414 metadata, at /.well-known/oauth-authorization-server on its host.

Send the person to authorize​

Redirect the person's browser to the authorize endpoint. Every parameter is required.

GET https://auth.andopen.co/oauth/authorize
?response_type=code
&client_id=<your client_id>
&redirect_uri=<your registered redirect URI>
&scope=gifting:write
&state=<random, unguessable value>
&code_challenge=<PKCE challenge>
&code_challenge_method=S256
&resource=https://api.andopen.co/2026-05

When the person comes back, check that state matches the value you sent.

Exchange the code for tokens​

From your server, post the code to the token endpoint along with your client secret and the PKCE verifier:

curl -X POST "https://auth.andopen.co/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<code>" \
-d "redirect_uri=<your registered redirect URI>" \
-d "client_id=<your client_id>" \
-d "client_secret=<your client secret>" \
-d "code_verifier=<PKCE verifier>"
Response · 200
{
"access_token": "eyJraWQiOiJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200a1b2...",
"scope": "gifting:read gifting:write"
}

You asked for gifting:write, and the token lists gifting:read gifting:write. That's expected: write includes read.

Call the API​

Send the access token in the Authorization header and the API version in AndOpen-API-Version. Both are required on every call.

curl "https://api.andopen.co/gift_requests" \
-H "Authorization: Bearer <access token>" \
-H "AndOpen-API-Version: 2026-05"

Refresh the access token​

Access tokens last one hour. Refresh with a standard refresh_token grant:

curl -X POST "https://auth.andopen.co/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=<refresh token>" \
-d "client_id=<your client_id>" \
-d "client_secret=<your client secret>"

Each refresh returns a new refresh token. Store it and discard the old one.

A refresh token expires after 30 days without use. If you refresh at least once every 30 days, the connection keeps working. A refresh also fails once the customer's account is no longer switched on for OAuth. Either way you get invalid_grant, and the person has to connect again.

Common pitfalls​

Send the resource for the customer's region on every authorize request. It tells &Open which API the token is for, and it must match exactly. Not every OAuth library sends resource by default, so check yours. A missing value returns invalid_request and a value the authorization server doesn't recognize returns invalid_target. In both cases the person never reaches the consent screen.

PKCE with S256 is required, even though your app holds a client secret. Some OAuth libraries skip PKCE for confidential clients, so check that yours sends it. The authorization server also refuses a request without state.

A token whose audience the API doesn't accept is rejected with 401 and invalid_audience:

Response · 401
{
"errors": [
{
"type": "authentication_error",
"code": "invalid_audience",
"message": "The token's audience is not this API. Re-authorize with the resource parameter set to this API's audience."
}
]
}

Refreshing doesn't fix it, because the refreshed token carries the same audience. Send the person through the flow again with the right resource.

What your app can reach​

The only scope you request is gifting:write.

API resourceCovered
gift_requests: list, read, create, cancelYes. A user-level grant sees the gift requests the connected person sent, or every gift request in the account if they're an &Open admin. An account-level grant sees every gift request.
campaigns: list, readYes
users/me: the connected person's own recordYes, for a user-level grant. An account-level grant names no user, so it gets 400.
users: the account's user directoryNo. Listing users returns 403 forbidden. Reading a user returns 404 for anyone but the connected person, and for every user on an account-level grant.

Scope names stay stable once issued. If you need more access later, we add a new scope beside gifting:write and never reduce what it already covers.

Endpoints​

The grant endpoints are on auth.andopen.co (auth.eu.andopen.co in the EU). The API is on api.andopen.co (api.eu.andopen.co).

MethodPathPurpose
GET/oauth/authorizeStart the flow
POST/oauth/tokenExchange a code, or refresh a token
POST/oauth/revokeRevoke a token when someone disconnects in your app
POST/oauth/introspectCheck a token's state while debugging. Don't call it on every request.
GET/.well-known/oauth-authorization-serverIssuer metadata
GET/oauth/discovery/keysSigning keys (JWKS)
GETapi.andopen.co/.well-known/oauth-protected-resource/2026-05API metadata. See Get the protected-resource metadata.

Rate limits​

EndpointLimitCounted per
GET /oauth/authorize60 a minuteIP address
POST /oauth/token120 a minuteYour app and each &Open account
POST /oauth/revoke60 a minuteYour app
POST /oauth/introspect60 a minuteYour app

The token limit is counted per &Open account, so your headroom stays the same as more customers connect. Going over returns 429 with a Retry-After header and a JSON body your OAuth library can parse.

Errors and what to do​

API errors use the standard {"errors": [{"type", "code", "message", "param"}]} shape described in Errors. The grant endpoints return OAuth-standard errors with error and error_description.

What you getWhat it meansWhat to do
429Rate limitedRetry after the Retry-After interval.
401 with unauthorizedThe access token is no longer valid.Refresh once. If that fails, ask the person to connect again. A token issued in one region also gets unauthorized in the other, so check you're calling the region the person connected in before you refresh.
401 with invalid_audienceThe token was issued for a resource this API doesn't accept.Don't refresh. Run the flow again with the right resource.
400 with invalid_grantThe refresh token is no longer valid: it was revoked, lapsed, or reused, the person or account is no longer active, or the account is no longer switched on (account_not_entitled).Ask the person to connect again.
403 with forbiddenThe call is outside what your app can do.Don't retry.
invalid_request or invalid_target at authorizeA required parameter is missing, or resource isn't recognized.Fix the request.
400 with unsupported_api_versionThe AndOpen-API-Version header is missing or wrong.Don't retry. Send AndOpen-API-Version: 2026-05.
access_denied with account_not_entitledThe person's &Open account isn't switched on for OAuth.Ask the person to contact their &Open account manager.
access_denied with admin_consent_requiredYour app uses an account-level grant, and the person isn't an admin.Ask them to have an &Open admin connect.
access_deniedThe person chose Deny on the consent screen.Let them try again when they're ready.
5xxSomething went wrong on our side.Retry with backoff.

Of the error_description values, account_not_entitled and admin_consent_required are stable enough to branch on.

Disconnecting​

When someone disconnects &Open inside your app, call POST /oauth/revoke so the connection is removed on our side too.

An &Open admin can also see and revoke connected apps under Connected apps on the Settings page.

Keeping connections secure​

  • Keep the client secret on your server. Never ship it to a browser.
  • Send tokens in the Authorization header only. The API rejects tokens in query strings.
  • Don't log access tokens, refresh tokens, authorization codes, PKCE values, or your client secret.
  • Treat the sub claim in the access token as an opaque ID, and don't parse it.

Versions and changes​

This page covers version 2026-05 of the API. When a new API version arrives, you run the flow again with that version's resource value. Your 2026-05 tokens keep working for as long as 2026-05 is available. See Versioning.

Anything that could break your integration comes with a direct notice from us and a migration window we agree with you. Additions such as new fields, error codes, or scopes can ship at any time, so ignore anything you don't recognize.