IP-based access (CMS integration)
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 API integration guide. Use this one when:
- The end user is anonymous to the CMS (no Subrite OIDC session), but their network grants access (typical for institutional licenses — universities, libraries, corporate networks).
- A subscriber has configured IP ranges on their subscription via the Subrite admin or my-pages.
Prerequisites
- An M2M access token for the tenant, with the
content:readpermission. See the Third-Party Integration Guide for how to provision one. - A product in the catalog with "Allow access by IP range" enabled.
- One or more active subscriptions on that product, with IP ranges configured. Subscribers can manage their own ranges from my-pages, or a tenant admin can manage them on behalf of the customer.
The endpoint
- Method:
GET - Route:
{baseUrl}/api/v1/content-channels/by-ip?ip={clientIp} - Authorization:
Bearer <m2m-token> - Tenant scope: Inferred from the M2M token. The endpoint never returns content from another tenant.
Request
GET /api/v1/content-channels/by-ip?ip=203.0.113.42 HTTP/1.1
Host: api.subrite.no
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Response
200 OK with a JSON array of content channels the IP is entitled to. An empty array means no entitlement (which 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"
}
]
Picking the IP to send
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:
- Read the leftmost untrusted address from
X-Forwarded-For(or whatever header your reverse proxy / CDN sets), falling back to the socket peer address. - Validate the address against a strict IP parser. Reject anything that isn't a valid IPv4 or IPv6.
- Pass it through as
?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.
What not to send
- Do not send the IP address of your CMS server. The endpoint matches stored CIDRs against the
ipyou pass; passing the wrong address silently returns no channels. - Do not strip ports or paths — pass only the address.
Authorization model
Behavior is identical to other M2M endpoints:
- The token must be issued by Subrite for the tenant in question.
- The token must carry
content:read. - A 401 means the token is missing, expired, or signed with a key Subrite doesn't recognize.
- A 403 means the token is valid but lacks
content:read.
The endpoint does not consider the requester's own IP. Only the ip query parameter is matched.
Caching guidance
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:
- Cache by
(tenantId, ip)with a short TTL — five minutes is a reasonable starting point. - Invalidate on a fixed schedule rather than trying to listen for admin changes; admins may add or remove ranges at any time, so a short TTL is simpler than push-based invalidation.
- If you cache an empty result (no entitlement), use the same TTL — don't treat empty as "no answer yet" or you'll re-query continuously.
Error responses
| 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 content:read, or wrong tenant. |
| 200 | [] | No subscription on this tenant has a CIDR containing the given IP. |
A successful response always carries the same shape — an array of channels, possibly empty. Treat 200 [] and 200 [{...}] the same way: the array is the entitlement.
Examples
curl
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-ip
Node (fetch)
async 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();
}
Python (httpx)
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()
Putting it together in a CMS
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);
Out of scope
The endpoint deliberately does not:
- Identify which subscriber granted the access. The CMS only learns whether access exists, not whose subscription enabled it. If you need attribution, rely on standard request logs at Subrite (the underlying
member_package_product_idis logged with each call). - Enforce concurrency limits or seat counts. IP-based access is treated as "anyone behind these CIDRs."
- Geolocate the IP. If you need geolocation, do it in your CMS before deciding whether to call this endpoint.
Testing your integration
In a non-production environment, the easiest way to verify an integration end-to-end:
- Create a subscription on a product with Allow access by IP range enabled.
- Add a CIDR that covers a test IP you can call from (your office IP, a known proxy, etc.).
- Call the endpoint with that IP and confirm the expected channel appears.
- Call again with an IP outside the configured ranges and confirm an empty array.
- Toggle the product flag off (after first removing the ranges) and confirm the lookup still works structurally — it just returns empty for IPs that previously matched.
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.