Developers
Two endpoints. One honest ad slot.
Call /api/serve-ad after your model responds, render what comes back beneath the answer, and let LoreAds handle matching, billing and payouts.
/api/serve-adSelects a campaign for the topic, records one impression, splits the revenue. Called from your backend only. Never expose the API key in browser code.
/api/track-click?i=…Records the click on the impression and redirects the reader to the advertiser. Use the click_url exactly as returned.
Request
One small payload.
Send the API key from your dashboard from your server only, plus a short, non-sensitive topic signal. A few keywords are enough, up to 300 characters. Never send a transcript.
curl -X POST https://your-loreads-domain/api/serve-ad \
-H "Content-Type: application/json" \
-d '{
"api_key": "lr_live_your_key",
"context": "how do I choose a laptop for software development"
}'{ "ad": null, "reason": "no_eligible_campaign" }Response
Everything you need to render.
Return ad: null quietly when there is no fill. Never render an empty sponsored slot.
{
"ad": {
"impression_id": "6f2a…",
"campaign_id": "b91c…",
"headline": "Developer Tool Kit",
"body": "Tools and templates for your next software project.",
"cta_label": "Explore tools",
"click_url": "https://your-loreads-domain/api/track-click?i=6f2a…",
"disclosure": "Sponsored"
},
"topic": "software"
}ChatGPT
Pair ChatGPT with one LoreAds call.
The most common integration: your backend asks ChatGPT for the answer, then asks LoreAds for one matching recommendation. The topic signal is the user's question only — never the transcript. Try it first in the live demo.
// server.js — one endpoint: ChatGPT reply + one LoreAds ad.
// ENV: OPENAI_API_KEY, LOREADS_API_KEY
app.post("/api/chat", async (req, res) => {
const { messages } = req.body; // [{ role, content }]
// 1. Reply from ChatGPT
const ai = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({ model: "gpt-4o-mini", messages }),
});
const aiJson = await ai.json();
const reply = aiJson.choices[0].message.content;
// 2. Topic signal from the user's last message only (not the transcript)
const topicSignal = String(messages.at(-1).content).slice(0, 300);
// 3. One sponsored recommendation (billed as a single impression)
const lr = await fetch("https://your-loreads-domain/api/serve-ad", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: process.env.LOREADS_API_KEY,
context: topicSignal,
}),
});
const { ad } = await lr.json();
// 4. Return both — render the ad beneath the answer when present
res.json({ reply, ad });
});SDK
A drop-in renderer, no dependencies.
// SERVER-SIDE ONLY. Store your API key as LOREADS_API_KEY.
app.post("/api/loreads-ad", async (req, res) => {
const response = await fetch("https://your-loreads-domain/api/serve-ad", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: process.env.LOREADS_API_KEY,
context: String(req.body.topicSignal || "").slice(0, 300),
}),
});
const payload = await response.json();
res.status(response.status).json({ ad: payload.ad || null });
});
// Browser: call your own backend route; never expose the LoreAds key.
async function renderSponsoredCard({ topicSignal, mount }) {
const response = await fetch("/api/loreads-ad", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ topicSignal }),
});
const { ad } = await response.json();
if (!ad) return;
const card = document.createElement("section");
const label = document.createElement("span"); label.textContent = ad.disclosure;
const headline = document.createElement("p"); headline.textContent = ad.headline;
const body = document.createElement("p"); body.textContent = ad.body;
const link = document.createElement("a"); link.href = ad.click_url;
link.rel = "sponsored noopener"; link.textContent = ad.cta_label;
card.append(label, headline, body, link); mount.replaceChildren(card);
}Rules
What you send, and what you show.
- Send a topic, not a transcript. Never include names, emails, account IDs, health details or the full conversation in context. Health and wellbeing topics are excluded from ad matching.
- Always render the disclosure. The disclosure field reads “Sponsored” and keep it visible above or beside the recommendation.
- Do not edit the creative. Headline, body and call to action are reviewed before they reach you.
- Do not cache the response. Each call is one impression and one match.
- Do not spoof location headers. Location targeting uses trusted Cloudflare edge metadata only; custom x-country or x-city headers are ignored. City targeting may not match when the edge does not provide a city.
Reference
Reasons a slot stays empty.
live_delivery_disabledHTTP 503: commercial delivery is paused until billing and compliance approval. Publisher integration can be tested using the isolated lab.missing_api_keyNo api_key in the request body.invalid_api_keyKey not recognised. Rotate it in the dashboard.chatbot_pausedThe chatbot is paused by its publisher.publisher_inactivePublisher account is not active.missing_contextSend a short, non-empty topic signal (up to 300 characters).context_too_largeThe topic signal exceeds 1,200 UTF-8 bytes.request_too_largeThe JSON request exceeds the 8 KB request limit.rate_limitedThis API key exceeded the per-instance burst limit. Retry after the Retry-After interval.publisher_rate_limitedHTTP 429. This publisher reached the shared request limit of 120 requests per minute across all registered chatbots. Retry after Retry-After.publisher_quota_reachedHTTP 429. This publisher reached the shared impression limit of 30 per minute or 1,000 per UTC day across all registered chatbots. Retry after Retry-After.quota_unavailableThe shared publisher quota could not be checked safely. No ad was billed; retry shortly.no_campaignsNo campaign is live yet. Retry later.no_topic_matchThe topic signal did not map to a supported topic.no_relevant_campaignNo approved campaign targets the detected topic.no_eligible_campaignMatching campaign budgets or dates are not eligible today.Topics
The matching vocabulary.
Campaigns choose from these topics; your topic signal is matched against them. Nothing else about the conversation is used.
Need your API key? Sign in and open your chatbot.
Official SDK
One script, two functions.
Load the dependency-free renderer once, call serve() from your backend with your API key, then render() the returned ad beneath the answer. Your API key never reaches the browser.
<!-- Browser: load once. ~1 KB, no dependencies. -->
<script src="https://your-loreads-domain/api/loreads-sdk"></script>// Server (Node.js): your API key stays on your server.
const { ad } = await LoreAds.serve({
apiKey: process.env.LOREADS_API_KEY,
context: approvedTopic, // topic signal only
endpoint: "https://your-loreads-domain",
});
// Forward `ad` to your frontend; render it beneath the answer.<!-- Browser: render the ad your backend returned. -->
<div id="loreads-slot"></div>
<script>
// `ad` is the object your backend sent down.
LoreAds.render(ad, "#loreads-slot");
</script>Testing
Verify your integration.
Run a no-billing integration test from your publisher dashboard. It exercises key validity, topic matching, the response shape and the click redirect — without recording a real impression or charging an advertiser.
When you are ready to go live, run the curl above from your own server. Each live call is one billed impression.
Connect your first chatbot today.
Register a chatbot and integrate the API. Ads are served when an eligible campaign matches the topic and targeting.