Integration docs
Three ways to put a unit on your page, all served from plainsun.com. Pick the one that fits your stack — they return the same ad and count the same way.
Your site key comes from the site’s own page on your dashboard, which also carries the tags with the key already filled in. Everything below uses YOUR_SITE_KEY in its place.
What we won’t do
No cookie on your page. Not one, not ever.
No fingerprinting, and no profile of anybody.
No raw IP address kept anywhere — the working is on the privacy page.
No renaming /f.js to dodge a filter list. When it is blocked, path 2 is the answer.
And no counting a viewable impression you did not serve: the beacon needs the signed, single-use token from the unit response, so the number on your public page is one we can stand behind.
Three ways in
All three carry the disclosure mark, and it is not optional on any of them.
1 · Script and placeholder
Progressive enhancement. Your own fallback content shows if JavaScript is off or /f.js is filter-listed, and the script replaces it when it runs.
<script async src="https://plainsun.com/f.js" data-site="YOUR_SITE_KEY"></script>
<link rel="stylesheet" href="https://plainsun.com/f.css">
<div data-daylight>
<a href="https://your-site.example">Your fallback content — publisher-owned</a>
</div>
The stylesheet is optional — the unit is semantic markup and works unstyled. Layout and colours come from the site’s own page on your dashboard, and your own CSS overrides any of it.
script-src https://plainsun.com;
style-src https://plainsun.com;
img-src https://plainsun.com;
connect-src https://plainsun.com;
2 · Raw JSON API
Server-side, in-app, or in a newsletter. No JavaScript on the page at all, which is what makes this one survive ad blockers and strict policies.
GET https://plainsun.com/api/v1/unit?site=YOUR_SITE_KEY
Pass &nonce= a random per-page-view string and the server will not hand you the same ad twice on that page (three units maximum per page). With nothing to serve you get 200 and {"ad": null} — not an error.
Rate limit: 60 requests a minute per address. Past that, 429 with Retry-After.
3 · Barebones markup
A compliant hand-rolled unit, for when you want to render it yourself. Substitute the placeholders from the JSON above.
<article class="ad" role="complementary">
<a href="{{go_url}}" target="_blank" rel="noopener noreferrer sponsored nofollow">
<img src="{{image_url}}" alt="" width="96" height="96">
<h3>{{title}}</h3>
<p>{{description}}</p>
</a>
<p class="disclosure">
<a href="{{source_url}}">{{disclosure.source_text}}</a> ·
<a href="{{network_url}}">via {{network_text}}</a>
</p>
</article>
The whole card wraps the link to go_url, and the disclosure line stays visible. That is the one part of a unit a publisher may not restyle away.
What the API returns
One ad, everything needed to render it, and everything needed to disclose it.
The unit response
{
"ad": {
"id": 42,
"title": "Cursor",
"description": "The AI-first code editor.",
"image_url": "https://plainsun.com/img/<hash>.webp",
"product_url": "https://cursor.sh",
"product_domain": "cursor.sh",
"source_board": "rankbid.lol",
"source_url": "https://plainsun.com/go/<signed-credit-token>",
"category": "devtools",
"image_is_generated": false
},
"go_url": "https://plainsun.com/go/<signed-token>",
"impression_token": "<signed, single-use, 1h>",
"disclosure": {
"required": true,
"source_text": "Ad · via rankbid.lol",
"source_url": "https://plainsun.com/go/<signed-credit-token>",
"network_text": "Daylight",
"network_url": "https://plainsun.com/"
},
"site": {
"slug": "your-site-example",
"layout": "card",
"theme": { "bg": "#ffffff", "fg": "#111111", "accent": "#1a56ff" }
}
}
image_url is never null: a listing with no usable image gets a generated card in its category colour, flagged by image_is_generated. site.layout and site.theme carry the choices from the site’s own page, so a server-rendered unit can honour them too.
The disclosure is not optional. Both source_text and network_text stay visible, and source_text links to disclosure.source_url when it is set. That link is tracked: it lands on the board with the same ?ref=, and the click is counted for the board and never as a click on the product. It is null for a listing with no board, in which case the text stands alone.
Counting a viewable impression
optionalEvery unit response carries an impression_token: signed, valid for an hour, redeemable once. Post it back when the unit has actually been seen and it counts as a viewable impression on your public page. Skip it and the unit still counts as a raw impression — you just will not have the stronger number.
POST https://plainsun.com/api/v1/e
Content-Type: text/plain
{"impression_token": "<token from the unit response>"}
Sent as text/plain on purpose: it keeps the request CORS-simple, so the browser skips the preflight and navigator.sendBeacon works. The server reads the body and ignores the content type.