IP-based access
Server-to-server endpoint that returns the Content Channels a client IP address is entitled to, for institutional access by IP range. Covers authorization, which IP to send, caching, errors and code examples.
Server-to-server endpoint that returns the Content Channels a client IP address is entitled to, for institutional access by IP range. Covers authorization, which IP to send, caching, errors and code examples.
This endpoint is for server-to-server checks from a CMS. It answers a single question: given a client IP, which Content Channels should a request originating from that IP be allowed to see?
It complements the per-user access (OIDC) guide. Use this one when:
content:read permission. See the third-party API integration guide for how to provision one.GET{baseUrl}/api/v1/content-channels/by-ip?ip={clientIp}Bearer <m2m-token>GET /api/v1/content-channels/by-ip?ip=203.0.113.42 HTTP/1.1
Host: api.subrite.no
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...200 OK with a JSON array of content channels the IP is entitled to. An empty array means no entitlement. This is also the response for an unrecognized IP; there is no separate "not found" status.
[
{
"id": 42,
"name": "University Library Online",
"description": "Premium library content"
}
]Your CMS must extract the end user's client IP and pass it as the ip query parameter. The endpoint is server-to-server; it never sees the originating request, so it cannot guess.
A correct pipeline typically looks like this:
X-Forwarded-For (or whatever header your reverse proxy or CDN sets), falling back to the socket peer address.?ip=....The endpoint accepts both IPv4 (1.2.3.4) and IPv6 (2001:db8::1). IPv4-mapped IPv6 addresses (::ffff:1.2.3.4), which are common when an Express/Node app sits behind certain proxies, are normalized server-side to their IPv4 form before lookup, so you don't have to handle that yourself.
ip you pass; passing the wrong address silently returns no channels.Behavior is identical to other M2M endpoints:
content:read.content:read.The endpoint does not consider the requester's own IP. Only the ip query parameter is matched.
Each request hits the database for an indexed cidr >>= inet lookup. The query is cheap, but if your CMS has high request volume you should not call this endpoint per page view. Recommended pattern:
(tenantId, ip) with a short TTL. Five minutes is a reasonable starting point.| Status | messageCode | Cause |
|---|---|---|
| 400 | invalidIpAddress | The ip query parameter is not a valid IPv4 or IPv6 address. |
| 400 | (validation error) | ip is missing or not a string. |
| 401 | (unauthorized) | Token missing, expired, or signature invalid. |
| 403 | forbiddenResource | Token lacks , or wrong tenant. |
A successful response always has the same shape: an array of channels, possibly empty. Treat 200 [] and 200 [{...}] the same way: the array is the entitlement.
curl -sS \
-H "Authorization: Bearer ${SUBRITE_M2M_TOKEN}" \
--get --data-urlencode "ip=203.0.113.42" \
https://api.subrite.no/api/v1/content-channels/by-ipasync function channelsForIp(clientIp: string): Promise<Array<{ id: number; name: string }>> {
const url = new URL('/api/v1/content-channels/by-ip', process.env.SUBRITE_API_BASE);
url.searchParams.set('ip', clientIp);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SUBRITE_M2M_TOKEN}` },
});
if (!res.ok) {
throw new Error(`Subrite by-ip lookup failed: ${res.status}`);
}
return res.json();
}import os
import httpx
def channels_for_ip(client_ip: str) -> list[dict]:
res = httpx.get(
f"{os.environ['SUBRITE_API_BASE']}/api/v1/content-channels/by-ip",
params={"ip": client_ip},
headers={"Authorization": f"Bearer {os.environ['SUBRITE_M2M_TOKEN']}"},
)
res.raise_for_status()
return res.json()Pseudocode for an article page handler:
const clientIp = extractClientIp(request); // your reverse-proxy-aware extractor
const requiredChannelId = article.requiresContentChannelId;
const channels = await cache.getOrSet(
`subrite:by-ip:${tenantId}:${clientIp}`,
() => channelsForIp(clientIp),
{ ttlSeconds: 300 },
);
if (channels.some((c) => c.id === requiredChannelId)) {
return renderFullArticle(article);
}
return renderPaywall(article);The endpoint deliberately does not:
member_package_product_id is logged with each call).In a non-production environment, the easiest way to verify an integration end-to-end:
Subrite's own integration tests cover the IPv4/IPv6 matching, IPv4-mapped IPv6 normalization, ACTIVE-only package filtering, and tenant scoping, so you can rely on those behaviors and focus your tests on your own IP-extraction pipeline.
content:read| 200 | [] | No subscription on this tenant has a CIDR containing the given IP. |