API overview¶
Reference for integrating the mobile app with the CS Lewis API. All endpoints are
served under /api/.
Content and Journeys. For the full app-flow contract, see Mobile API full app flow. It covers config, the Home feed, reading, Journeys, and progress and bookmarks, with example payloads. This contract is a proposed mock, pending FE confirmation.
Endpoint guides. Each guide covers an endpoint's concept and behaviour, and the exact schemas are in Swagger. See the Passages API guide, the Themes API guide, and the Home API guide.
Environments¶
| Surface | Dev | Prod |
|---|---|---|
| Host | dev-cslewis.fueled.engineering |
Not deployed yet |
| API base | https://dev-cslewis.fueled.engineering/api/ | Not deployed yet |
| OpenAPI schema | https://dev-cslewis.fueled.engineering/api/schema/ | Disabled |
| Swagger UI | https://dev-cslewis.fueled.engineering/api/docs/ | Disabled |
| Redoc | https://dev-cslewis.fueled.engineering/api/redoc/ | Disabled |
| Wagtail CMS (non-API) | https://dev-cslewis.fueled.engineering/studio/ | Unguessable prefix |
| Django admin (non-API) | https://dev-cslewis.fueled.engineering/backstage/ | Unguessable prefix |
Notes:
- The schema, Swagger, and Redoc interactive reference is served only in non-production
environments (
API_DOCS_ENABLED), so it is disabled in prod. - Admin route prefixes are configurable per environment (
WAGTAIL_ADMIN_URLandDJANGO_ADMIN_URL). Prod uses unguessable values, which are not published here.
App key¶
Every request must include a shared app key in the X-API-Key header:
The mobile team receives the key out of band, and it differs per environment. A
missing or unknown key returns 403 with error_type: "InvalidAPIKey". This key
identifies the app, not a user. For user identity, see Authentication.
Authentication¶
User identity uses Firebase. The app signs in with the Firebase SDK (a passwordless email link) and sends the resulting ID token as a bearer credential on every request that needs a user, alongside the app key:
Content reads are guest-open (app key only). Endpoints that act on a user
require the bearer token and return 401 without one.
There is no registration endpoint. The backend provisions the local account
on the first request that carries a valid token ("created on first sight"). After
sign-in, call GET /api/account/ once. It returns the profile and guarantees
the account exists.
A 401 means the token was missing, invalid, or expired (errors[0].message
says which). Refresh the ID token with the Firebase SDK and retry. The backend
creates an account only from a token with a verified email. The email-link flow
always produces one.
Account endpoints¶
| Endpoint | What it does |
|---|---|
GET /api/account/ |
Profile of the signed-in user (uid, email, name, joined_at). Provisions the account on first call. |
DELETE /api/account/ |
Permanent account deletion (App Store mandate). Optional JSON body {"reason": "..."}. Returns 204. |
Deletion removes the Firebase identity first (with the caller's own ID token),
then the local account and its data. Two outcomes other than 204 need handling:
401means Firebase wants a recent sign-in before a destructive action. Re-authenticate the user and retry.502means the identity provider could not be reached. Nothing was deleted, so the call is safe to retry.
Dev login: testing without a Firebase token¶
Non-production environments provide a bypass, so QA, API tooling, and Swagger try-it-out never need a real Firebase token:
The response:
The endpoint creates the account if it does not exist, so you can test fresh-user
flows with a new email. Use the returned token exactly like a Firebase ID token
(Authorization: Bearer ...). Swagger's Authorize dialog accepts it too. The
endpoint is gated by DEV_AUTH_ENABLED and returns 404 in production.
Versioning¶
Select the API version with the Accept header. There is no version segment
in the URL:
1.0 is the default and currently the only supported version. An unsupported
version returns 406.
The server also reports an advisory version at GET /api/config/:
Read api_version on launch to decide whether to prompt an update. It is advisory
only, and the server does not check it per request.
Errors¶
Every error response uses one envelope:
{
"error_type": "ValidationError",
"errors": [
{ "field": "email", "message": "This field is required." },
{ "message": "A non-field error." }
]
}
error_typeis a stable, machine-readable code. Branch on it.errorsis always a list. Field errors carry afieldkey, and non-field errors omit it.messageis human-readable and safe to display.
Status codes follow HTTP conventions: 400 bad request or validation, 403
forbidden (including a bad app key), 404 not found, 406 unsupported version,
409 conflict, and 500 server error.
Pagination¶
List endpoints use limit-offset pagination, with a default page size of 30:
{
"count": 240,
"next": "...?limit=30&offset=90",
"previous": "...?limit=30&offset=30",
"results": []
}
Images¶
Where an endpoint says so below, the API serves a content image as a rendition set, one URL per standard width, rather than a single link to the original upload.
"image": {
"small": { "url": "https://.../chivalry.width-360.format-webp.webp", "width": 360, "height": 240 },
"medium": { "url": "https://.../chivalry.width-720.format-webp.webp", "width": 720, "height": 480 },
"large": { "url": "https://.../chivalry.width-1080.format-webp.webp", "width": 1080, "height": 720 },
"xlarge": { "url": "https://.../chivalry.width-1440.format-webp.webp", "width": 1440, "height": 960 },
"xxlarge": { "url": "https://.../chivalry.width-2048.format-webp.webp", "width": 2048, "height": 1365 }
}
| Key | Nominal width |
|---|---|
small |
360px |
medium |
720px |
large |
1080px |
xlarge |
1440px |
xxlarge |
2048px |
- Pick the width you need. The ladder does not depend on any one screen, so it stays stable as screens change.
- Every key is always present. Switching size never needs a second request.
- The field is
nullwhen the piece carries no image. It is never an empty object. widthandheightare the rendition's real dimensions. Use them to reserve layout space before the image decodes.- Renditions are never upscaled. Trust
width, not the key name: an 800px-wide upload reportswidth: 800underxlargeandxxlarge, not 1440 or 2048. - Images are WebP. Each URL is content-addressed by its filter, so a client can cache a URL indefinitely.
Where this applies. Every content image in the API uses a rendition set: the image on a
content card (What's New, Further Reading, the Home sections, a theme's pieces carousel, and the
related rails), the Writing and Article detail image, and a Book's cover wherever a book card
appears (Featured Books, and a passage's or writing's source).
Theme assets are the one exception. design.portal_icon and design.background stay single
original URLs, because they are pre-optimised Figma exports, so re-encoding them would only lose
quality.
The backend produces renditions when an editor saves the piece, not when you first request it, so a published image is already resized by the time it reaches you. ADR-0009 settles the sizes and how they are generated.
Caching¶
Public reads combine a short public cache window with an ETag for cheap revalidation:
- Within
max-age, a client or CDN serves from cache with no round-trip. That is the performance win of public caching. - After
max-agelapses, send the lastETagback asIf-None-Match. If the content is unchanged, the server returns304 Not Modifiedwith no body. If the content changed (an editor published or edited it), the server returns a fresh200with a newETag.
The window is configurable on the server (API_PUBLIC_CACHE_SECONDS, default 30s) and tuned per
environment. Set it lower for fresher content (for example, 0 in dev and QA, which revalidates every
request through the ETag), or higher to rely on caching more. Staleness is bounded by the window,
and revalidation after it costs only headers. Sending If-None-Match is optional. Omit it, and you
get a fresh 200 once the cache expires.