Skip to content

Your first call

This walkthrough ends with a working list_businesses response in your terminal. It takes about 5 minutes if you have your client ID ready.

  • A Caramel account at caramelme.com.
  • An OAuth client ID. If your redirect URI is http://localhost:3000/callback or another localhost URL, you can self-register in step 2 — no manual setup needed. Otherwise, email aymen@reactmotion.com with your app name, redirect URIs, and requested scopes.
  • curl installed locally.
Terminal window
curl -s https://app.caramelme.com/.well-known/oauth-authorization-server | \
python3 -m json.tool

You should see a JSON document with authorization_endpoint, token_endpoint, and registration_endpoint. If this call fails, check your network connection before continuing.

Skip this step if you already have a client ID issued manually.

Register a client for http://localhost:3000/callback:

Terminal window
curl -s -X POST https://api.caramelme.com/functions/v1/mcp-oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "my-first-app",
"redirect_uris": ["http://localhost:3000/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'

Copy the client_id from the response:

{
"client_id": "my-first-app",
"redirect_uris": ["http://localhost:3000/callback"]
}
Terminal window
export CLIENT_ID="my-first-app"
export REDIRECT_URI="http://localhost:3000/callback"

Run this Node.js snippet to get a verifier and challenge. Keep both — you’ll need them in the next two steps.

// save as pkce.mjs, run with: node pkce.mjs
import crypto from 'node:crypto';
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
console.log('VERIFIER: ', verifier);
console.log('CHALLENGE:', challenge);
Terminal window
node pkce.mjs
# VERIFIER: 5rq1oKGcE9...
# CHALLENGE: X3Lx8pMkT0...
export VERIFIER="5rq1oKGcE9..."
export CHALLENGE="X3Lx8pMkT0..."

Alternatively, in Python:

# save as pkce.py, run with: python3 pkce.py
import secrets, hashlib, base64
verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b'=').decode()
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip(b'=').decode()
print('VERIFIER: ', verifier)
print('CHALLENGE:', challenge)

Open this URL in a browser. Replace the placeholder values with your own:

https://api.caramelme.com/functions/v1/mcp-oauth
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=http://localhost:3000/callback
&code_challenge=YOUR_CHALLENGE
&code_challenge_method=S256
&state=any-random-string
&scope=meta:read

Or build it in the terminal:

Terminal window
STATE=$(openssl rand -hex 16)
echo "Open this URL in your browser:"
echo "https://api.caramelme.com/functions/v1/mcp-oauth?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&code_challenge=${CHALLENGE}&code_challenge_method=S256&state=${STATE}&scope=meta%3Aread"

Enter your Caramel email address. Caramel sends a magic-link email. Click the link.

The browser redirects to http://localhost:3000/callback?code=AUTH_CODE&state=…. Nothing runs at that URL yet — that’s fine. Copy the code parameter from the browser’s address bar.

Terminal window
export AUTH_CODE="the-code-from-the-url"
Terminal window
curl -s -X POST https://api.caramelme.com/functions/v1/mcp-oauth \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=${AUTH_CODE}" \
-d "redirect_uri=${REDIRECT_URI}" \
-d "client_id=${CLIENT_ID}" \
-d "code_verifier=${VERIFIER}" | python3 -m json.tool

You get back:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "rt_8f3e21c4b7d94a1f8e2c6b5d4a3f2e1d",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "meta:read"
}
Terminal window
export TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Note The authorization code is single-use. If you get 400 invalid_grant, start again at step 4 — the code either expired (codes are short-lived) or was already exchanged.

Terminal window
curl -s -X POST https://app.caramelme.com/api/functions/caramel-mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 1,
"params": { "name": "caramel.v1.business.list", "arguments": {} }
}' | python3 -m json.tool

You should see the businesses your account has access to:

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"businesses\":[{\"id\":\"bus_01h7m9k0aabcdefghj1234567\",\"name\":\"Brewmaster Café\",\"tier\":\"growth\",\"role\":\"owner\"}]}"
}
]
}
}

Parse the result.content[0].text field — it’s a JSON string that contains the actual payload.

Terminal window
export BUSINESS_ID="bus_01h7m9k0aabcdefghj1234567"

That’s it. You now have a working token and a business ID. Every other tool call follows the same shape — change the name and arguments fields.

SymptomLikely causeFix
400 invalid_grant on code exchangeCode expired or already usedRepeat step 4 to get a fresh code
400 invalid_grant on refreshRefresh token rotated outRe-run the full authorize flow
401 unauthorized on tool callAccess token expiredRun the refresh call; see Authentication
403 scope_requiredToken scope too narrowRe-authorize with the required scope
Empty businesses arrayNo businesses linked to this accountSign into caramelme.com and confirm at least one business exists
-32001 JSON-RPC errorToken expired mid-sessionRefresh the access token and retry the call
  • Quickstart — the same flow as a one-page reference card.
  • Authentication — refresh rotation, scope table, error codes, and edge cases.
  • Reference — every tool in the catalog.