OAuth & connectors

Authenticate users and call the multi-tenant API from any client

Integration guide

Connect your application

This API does not issue credentials itself. End users sign in through https://auth.gttwl.net (TAT OAuth). Your app receives a Bearer access token, then calls this API. Tokens are validated via OAuth introspection on every request.

API base URL

Set your connector's API base URL explicitly. Relative paths below are always on this host:

Environment Base URL
Production https://api-edge.gttwl.net
This deployment https://api-edge.gttwl.net
Local development http://localhost:4040

Not api.gttwl2.com

https://api.gttwl2.com is a different legacy service (org_api) and does not host /api/agencies, /api/posts, or /api/advisors. Calls there can return HTTP 200 with {"error":"Not found","status":404} (34 bytes) — that is not this API.

Architecture

  1. Register an OAuth client at /admin/clients on the OAuth server. Confidential clients receive a client_id and client_secret.
  2. Assign scopes appropriate to your product (see table below). This API enforces those scopes on every protected route (RFC 6750 insufficient_scope). Empty-scope tokens cannot call the API.
  3. Complete the authorization code + PKCE flow; exchange the code for access and refresh tokens.
  4. Call this API with Authorization: Bearer <access_token> and, for agency-scoped routes, X-Agency: <cname>.
  5. This API verifies each token at GET https://auth.gttwl.net/auth/introspect before serving data.

Centralized verification

Signature, expiry, issuer, and audience checks run on the OAuth server only. This API does not store OAuth clients or JWKS keys.

Recommended scopes

Scope names are configured on the OAuth client. A typical content connector uses:

Scope Purpose
openid Identity / userinfo (/auth/me)
profile Name and email on userinfo
agencies:read GET /api/agencies
categories:read GET /api/categories
posts:read GET /api/posts, GET /api/posts/:id
posts:write Create, update, and delete posts, attachments, itinerary lines, and FAQs

Recommended connector scope string: openid profile agencies:read categories:read posts:read posts:write

Step 1 — Register an OAuth client

In the TAT OAuth admin panel, create a client for your application:

  • client_id — public identifier embedded in the JWT aud claim
  • client_secret — required for confidential server-side apps (omit for public clients using PKCE only)
  • redirect_uris — exact callback URLs (dev and production)
  • Grant types — enable authorization_code and refresh_token

Deliver client_secret out of band. Store secrets in your credential vault — never in source control.

Step 2 — Authorization code + PKCE

PKCE is required (S256 only; plain is rejected). Generate a code_verifier and code_challenge, then redirect to:

https://auth.gttwl.net/oauth/authorize?scope=openid+profile+agencies%3Aread+categories%3Aread+posts%3Aread+posts%3Awrite&state=random-csrf-state&client_id=YOUR_CLIENT_ID&code_challenge=YOUR_CODE_CHALLENGE&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fyour-app.example%2Fauth%2Fcallback&response_type=code

After sign-in (and consent when scopes are assigned), the OAuth server redirects to your redirect_uri with a code query parameter. Exchange it:

POST https://auth.gttwl.net/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=YOUR_CLIENT_ID
&code=AUTHORIZATION_CODE
&redirect_uri=YOUR_REDIRECT_URI
&code_verifier=YOUR_CODE_VERIFIER

Confidential clients authenticate with HTTP Basic (client_id:client_secret) or include client_secret in the body.

Example token response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs…",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "opaque-refresh-token",
  "scope": "openid profile agencies:read categories:read posts:read posts:write"
}
Token Lifetime (confidential) Notes
Access 15 minutes RS256 JWT
Refresh 365 days Rotates on use; reuse of a revoked refresh chain-revokes

For background publishing, persist tokens in an encrypted server-side store and refresh outside the browser session. Keep only the latest refresh token; write it atomically.

Step 3 — Verify identity &amp; pick an agency

After token exchange, call userinfo to confirm the link. The sub claim is the TAT user zid (there is no separate zid field):

GET https://auth.gttwl.net/auth/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIs…

Then list agencies the user may publish to (Bearer only — no X-Agency):

GET https://api-edge.gttwl.net/api/agencies
Authorization: Bearer eyJhbGciOiJSUzI1NiIs…

Store the chosen cname as the connection preference and send it as X-Agency on every publish. Users with access to multiple agencies may maintain one connection per (zid, agency.cname) pair in your product.

Step 4 — Call this API

GET https://api-edge.gttwl.net/api/posts
Authorization: Bearer eyJhbGciOiJSUzI1NiIs…
X-Agency: travelonly.com
Header Required Description
Authorization Always Bearer <access_token>
X-Agency Agency routes Agency cname. Not required for GET /api/agencies.

Access token claims

{
  "sub": "user-zid-uuid",
  "aud": "your_client_id",
  "iss": "https://auth.gttwl.net",
  "iat": 1709123456,
  "exp": 1709124356,
  "scope": "openid profile agencies:read categories:read posts:read posts:write",
  "role": 3,
  "group": "cms_group_admin"
}
Claim Meaning
sub User zid — stable identity
aud OAuth client_id
scope Granted scopes (space-separated)
role 2 agent, 3 admin, 4 superadmin
group Role group string (e.g. cms_group_admin)
exp Unix expiry (15-minute access TTL)

Refresh tokens

When access expires, refresh (rotation issues a new refresh and revokes the old one):

POST https://auth.gttwl.net/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&client_id=YOUR_CLIENT_ID
&refresh_token=YOUR_REFRESH_TOKEN

Publishing content (recommended sequence)

Clients that publish rich posts should follow this sequence. The API does not rewrite HTML — weave CDN image URLs into content yourself after upload.

  1. POST /api/posts — title, summary, prose, optional SEO data, faqs, src/src_id, place. Omit type (defaults to blog) and category (defaults to permalink escorted-or-guided-tour; discover values via GET /api/categories). Response is enveloped as {"data": { "id", "permalink", "active", "place", … }}.
  2. POST /api/posts/{id}/attachments — multipart uploads; set primary=true for the hero. Preferred size 1200×630 (guidance only). A later primary=true upload re-designates the cover.
  3. PUT /api/posts/{id} — final HTML with CDN <img> URLs. When faqs is present it replaces the full set; omit the key to leave FAQs unchanged.

Authorship: omit author on create to attribute the post to the token user. Admin-grade callers may send author: {"zid": "…"} (4-check enforced). On update, omit author to keep the existing author (sticky). Discover targets via GET /api/advisors. Full field contracts live in the OpenAPI reference.

Environment variables

Typical client configuration:

OAUTH_SERVER_URL=https://auth.gttwl.net
OAUTH_CLIENT_ID=your_client_id
OAUTH_CLIENT_SECRET=your_client_secret
OAUTH_REDIRECT_URI=https://your-app.example/auth/callback
API_BASE_URL=https://api-edge.gttwl.net

This API needs OAUTH_ISSUER (or config :tatex_api, :oauth) so it can introspect tokens.

Error responses

Status When
401 Missing, expired, or invalid Bearer token
400 X-Agency missing on agency-scoped routes
403 Insufficient OAuth scope (insufficient_scope + WWW-Authenticate), or token valid but user cannot access the requested agency
404 Resource not found or not visible to this role
422 Validation failed (see field errors)

Next step

Use the interactive OpenAPI reference for request bodies, schemas, and try-it-out against a live token.

Open API reference
Tatex API · api-edge.gttwl.net OpenAPI 3 · Bearer JWT