You create, rotate and revoke content keys in the console, on the integrations
page, along with the list of origins each key may be used from.
The key is publishable
The key ships in your website’s source, where anybody can read it. That is intended, not an oversight: a browser fetching articles on your public site cannot hide a secret, so the key is not treated as one. It is returned in full at creation and remains viewable in the console afterwards. What makes that safe is the per-key origin allowlist. A request is served only when itsOrigin matches an origin you registered for that key, so a copy
of the key lifted from your page is of no use anywhere else. Rate limiting is
the second control. Key secrecy is not a control at all.
Presenting the key
The key travels as thekey query parameter, the same transport
POST /v1/track uses, because a <script> tag can build a URL and cannot
always set a header:
Naming a reader
Adduser_id to either article endpoint when the response should be evaluated for a member:
user_id was not sent.
Reader naming is an opt-in on each content key and is off by default. A platform operator enables it for the key that should receive member-specific answers. A key with the setting off treats user_id as absent, so the response is the strictest answer for a gated article.
The platform enforces the tier decision and withholds the article body when the resolved reader does not qualify. The identity is asserted by the integrator: the platform does not verify that the caller is the person represented by the supplied id. It only resolves that id to an active member of the key’s organization. A missing reader, an unknown id, a member in another organization, or an inactive member receives the strictest answer, which is the locked response for a gated article.
When a request names a reader, a successful response is sent with Cache-Control: no-store, including when reader naming is disabled or the id does not resolve. This keeps one member’s tier result out of another response’s cache. Anonymous successful responses keep Cache-Control: private, max-age=60; refusals carry no cache header.
The Origin header is required
Every request must carry anOrigin header naming a registered origin. This
holds for server-side callers too: there is no exemption for a request that is
not from a browser, because an exemption would make every published key
readable from anywhere.
Browsers set Origin themselves. If you call these endpoints from your own
backend, set it explicitly, as the curl example above does.
Matching is on the whole origin (scheme, host and port), after lowercasing and
dropping default ports. https://www.yourclub.example and
https://yourclub.example are different origins; register both if you serve
both. There are no wildcards.
A development origin is an entry like any other, so register it too while you
are building: http://localhost:5173 is http, not https, and its port is
part of it, so it matches nothing you registered for the live site. Add the one
your dev server actually prints, port included, and drop it from the allowlist
when you are done.
Rotation takes effect immediately
Rotating a key revokes the previous token in the same operation. The old token stops working on the next request, with no grace period. Deploy the new key to your site before you rotate, or expect a gap.Cross-origin requests
The endpoints answer a CORS preflight from any origin and echo the request origin back on the realGET, including on refusals. A preflight carries no
key, so there is nothing to enforce it against; the allowlist is applied to the
actual read. The practical consequence is a good one: a request from an
unregistered origin fails as a readable 403 your code can log, rather than as
an opaque browser CORS error.
What each failure looks like
Every response below is the standard error envelope, andcode is the stable
part. Refusals carry no Cache-Control header.
No key, an unknown key, a revoked key, or a key of another type
All four are the same401, on purpose: a prober cannot learn which check
failed, or whether a given token ever existed.
An origin that is not on the key’s allowlist
403, whether or not the key itself was valid. A missing Origin header, an
empty allowlist and a non-matching origin all land here.
The content module is not installed
Also a403, with a different message. The club’s articles still exist; the
API is switched off until the module is reinstalled.