Start
OAuth 2.0 against Auth0, in two flows. Discovery needs no token; quoting and checkout do.
Two things that cost people an afternoon
The audience is an identifier, not an address. It looks like a URL and is never fetched. Requesting a token without it, or trying to call it, are the two most common first mistakes.
Dynamic client registration is disabled on the tenant. An MCP client that registers itself will complete discovery and then fail at the authorize step with nothing useful in the error. This is not a dead end — it means your client, including any redirect URIs, is registered by hand instead. See request access.
Both flows issue tokens for the same audience against the same scopes. The grant differs; what a token is allowed to do does not.
| You are | Use |
|---|---|
| A server calling us as yourself, with no traveler present | Client credentials |
| A connector or app acting for a person who signs in | Authorization code with PKCE (S256) |
curl --request POST \
--url https://swisstouristy.eu.auth0.com/oauth/token \
--header 'content-type: application/json' \
--data '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"audience": "https://api.swisstouristy.com/public/v1",
"grant_type": "client_credentials"
}'Then send it as an ordinary bearer token. The scheme match is case-insensitive.
curl https://swisstouristy.com/api/public/v1/quotes \
--header "Authorization: Bearer $ST_ACCESS_TOKEN"For a connector acting on behalf of a person. PKCE is required and S256 is the only accepted challenge method: a public client ships no usable secret, so the proof key is the only thing protecting the code. Your redirect URI must be registered with us before the first attempt — an unregistered one fails at the authorize step.
# 1. Send the person here.
https://swisstouristy.eu.auth0.com/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
&audience=https://api.swisstouristy.com/public/v1
&scope=discovery%20quote%3Acreate%20quote%3Aread-own
&state=YOUR_OPAQUE_STATE
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256
# 2. Exchange the code they come back with.
curl --request POST \
--url https://swisstouristy.eu.auth0.com/oauth/token \
--header 'content-type: application/json' \
--data '{
"grant_type": "authorization_code",
"client_id": "YOUR_CLIENT_ID",
"code": "CODE_FROM_REDIRECT",
"redirect_uri": "YOUR_REGISTERED_REDIRECT_URI",
"code_verifier": "YOUR_ORIGINAL_VERIFIER"
}'The resulting token is used exactly as above. Ask only for the scopes you need: a token carrying checkout:prepare can hand a traveler to checkout, and one that does not, cannot.
Least privilege, with no superset. A broader scope is never accepted as a substitute for the right one, and there is no admin or wildcard scope on this surface.
| Scope | What it permits |
|---|---|
| checkout:prepare | Hand a traveler to Swiss Touristy's hosted checkout for a live quote of your own. Creates no booking and takes no payment. |
| discovery | Connect to the MCP server. Required by the MCP transport on every connection; no REST operation requires it, and the REST discovery endpoints are anonymous. |
| quote:create | Create a quote of your own. |
| quote:read-own | Read a quote you created. Never another caller's. |
discovery is required by the MCP transport on every connection, and by no REST operation — the REST discovery endpoints are anonymous. If you are integrating over MCP, request it; if you are only calling REST, you will never need it.
Missing, expired, malformed and insufficient-scope tokens all return the same AUTHENTICATION_REQUIRED. That is deliberate — telling a caller which of those it was is a probing aid, and a legitimate integration knows which token it sent. Repeated failures are throttled against their own tier, so a credential-guessing loop slows itself down.