API Documentation

OAuth

Building an app or AI assistant that other people connect to their account? Let them sign in and click Allow, instead of copying an API key.

Use OAuth when people connect their own account to something you built: an AI assistant, a reporting tool, an agency dashboard. They sign in with us, see exactly what your app is asking for, and click Allow. Your app never sees their password, nobody has to copy a key around, and they can disconnect it whenever they like.

Only reading your own account? An API key is simpler: see Authentication.

Using an MCP client? It probably does all of this for you.

Claude, ChatGPT and most other MCP clients find our sign-in server on their own: give them the server address and they open the Allow page for the person. The steps below are for when you build the connection yourself.

How it works

  1. 1 Your app registers once and gets a client id.
  2. 2 You send the person to our Allow page with a code challenge (PKCE).
  3. 3 They sign in, choose what your app may do, and click Allow.
  4. 4 We send them back to your app with a short-lived code.
  5. 5 Your app swaps the code for an access token, and uses it with the MCP server or the REST API.

Step by step

1. Register your app

Send your app's name and the address we should send people back to. You can register up to 10 return addresses. Keep the client id: it is not a secret, and there is no client secret.

curl
curl -X POST https://citednearby.com/oauth/register \ -H "Content-Type: application/json" \ -d '{"client_name":"Your App","redirect_uris":["https://yourapp.example.com/callback"]}'
JSON
{ "client_id": "Rk8sY2Vx3mQe9TfLqZ0bWn4uHd7JpA1cVs6GiXo2", "client_id_issued_at": 1792765196, "client_name": "Your App", "redirect_uris": [ "https://yourapp.example.com/callback" ], "grant_types": [ "authorization_code", "refresh_token" ], "response_types": [ "code" ], "token_endpoint_auth_method": "none" }

2. Send the person to the Allow page

First make a code verifier, a long random secret your app keeps, and its SHA-256 hash, the code challenge. Only the challenge goes in the link. Add a random state too, so you can check the answer is for the request you made.

shell
# a random secret (the code verifier) and its SHA-256 hash (the code challenge) CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '=+/' | cut -c1-64) CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')

Then open this link in their browser:

Link
https://citednearby.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fyourapp.example.com%2Fcallback&code_challenge=CODE_CHALLENGE&code_challenge_method=S256&state=RANDOM_STATE

3. They sign in and click Allow

The page shows your app's name, where they will be sent back to, and what it can read: businesses, scans, rankings, reviews, listings and campaigns. A box, ticked to start with, lets it start scans and campaigns too, which use their credits. If they untick it, your app can only read.

4. Get the code from the return address

We send them back to your return address with a code, your state, and our address as iss. Check that the state is yours before you go on. The code works once, for 10 minutes.

Return address
https://yourapp.example.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATE&iss=https%3A%2F%2Fcitednearby.com

If they click Cancel, you get error=access_denied instead of a code.

5. Swap the code for tokens

Send the code with the code verifier from step 2. The answer has an access token, which lasts 1 hour, and a refresh token.

curl
curl -X POST https://citednearby.com/oauth/token \ -d grant_type=authorization_code \ -d client_id=YOUR_CLIENT_ID \ -d code=AUTHORIZATION_CODE \ -d redirect_uri=https://yourapp.example.com/callback \ -d code_verifier=$CODE_VERIFIER
JSON
{ "access_token": "42|f3Gh8kLmN2pQrS5tUvWx9yZaBcDeFgHiJkLmNoPq", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "cT9vWq2xR7zL4mN8bK3jH6gF1dS5aP0oI9uY2tE7rW4qX8", "scope": "mcp" }

Send the access token as a Bearer token, just like an API key, to the MCP server or the REST endpoints.

Keeping the connection going

When the access token runs out, use the refresh token to get a new pair. Each refresh token works once and lasts 90 days: always save the new one from the answer.

curl
curl -X POST https://citednearby.com/oauth/token \ -d grant_type=refresh_token \ -d client_id=YOUR_CLIENT_ID \ -d refresh_token=REFRESH_TOKEN

To disconnect from your side, revoke the refresh token. Its access token stops working too. The answer is always 200.

curl
curl -X POST https://citednearby.com/oauth/revoke \ -d client_id=YOUR_CLIENT_ID \ -d token=REFRESH_TOKEN

People can also see and disconnect every connected app themselves, in their dashboard under Settings > AI Assistants. After that, your tokens are refused with 401 and they have to connect again.

Rules your app has to follow

  • PKCE with S256 is required. Without it, or with the plain method, the person is sent straight back to your app with an error.
  • Return addresses must use https. http works only for localhost, 127.0.0.1 and [::1]. App links such as cursor:// or vscode:// are allowed. No #fragment.
  • The redirect_uri you send must match a registered one exactly. If it does not, we show an error page and never send the person anywhere.
  • There are no client secrets: every app is a public client.
  • The only scope is mcp. You can leave scope out.
  • One address can register up to 20 apps an hour.

OAuth endpoints

Everything starts at the two discovery documents, which list the others.

Method Path What it is for
GET /.well-known/oauth-protected-resource Says which sign-in server protects the MCP server. Most clients start here.
GET /.well-known/oauth-authorization-server Lists every endpoint below and what they support.
POST /oauth/register Registers your app and gives it a client id.
GET /oauth/authorize The page where the person signs in and clicks Allow.
POST /oauth/token Swaps the code for tokens, and later refreshes them.
POST /oauth/revoke Ends a connection by revoking its token.

When something goes wrong

The token and registration endpoints answer errors the standard OAuth way: an error code and a description you can log.

JSON
{ "error": "invalid_grant", "error_description": "The code is invalid, expired or already used." }
  • invalid_grant The code or refresh token is wrong, expired or already used, or the code verifier does not match. Start the sign-in again.
  • invalid_client The client id is unknown, or the code was issued to another app.
  • invalid_request A required field is missing.
  • unsupported_grant_type Use authorization_code or refresh_token.
  • invalid_redirect_uri A return address breaks the rules above (registration only).