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
-
Register an OAuth client at
/admin/clientson the OAuth server. Confidential clients receive aclient_idandclient_secret. -
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. - Complete the authorization code + PKCE flow; exchange the code for access and refresh tokens.
-
Call this API with
Authorization: Bearer <access_token>and, for agency-scoped routes,X-Agency: <cname>. -
This API verifies each token at
GET https://auth.gttwl.net/auth/introspectbefore 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 JWTaudclaim -
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_codeandrefresh_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 & 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.
-
POST /api/posts— title, summary, prose, optional SEOdata,faqs,src/src_id,place. Omittype(defaults to blog) andcategory(defaults to permalinkescorted-or-guided-tour; discover values viaGET /api/categories). Response is enveloped as{"data": { "id", "permalink", "active", "place", … }}. -
POST /api/posts/{id}/attachments— multipartuploads; setprimary=truefor the hero. Preferred size 1200×630 (guidance only). A laterprimary=trueupload re-designates the cover. -
PUT /api/posts/{id}— final HTML with CDN<img>URLs. Whenfaqsis 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