Your first API call.
Get a free sandbox key by email and paste a quickstart for your use case to test the product search and lookup calls in your integration. Then switch to your own publisher key and earn commission on the sales you drive. The same capabilities reach AI agents through our MCP server (with OAuth sign-in) and HasOffers-compatible tools.
Clickwise for developers and AI agents · 1:28 · AI-generated narration · Transcript
Transcript
Clickwise has a public API and an MCP server, so an AI agent can find affiliate programs, search a GTIN product catalog and get tracked links.
You can start without an account. Ask for a sandbox key by email: the link reveals it once, and it lasts fourteen days for product search and GTIN lookup.
Add the MCP server to Claude Code, Cursor or ChatGPT. Programs, deals, news and catalog statistics work with no signup; the sandbox key or your affiliate key covers product search, and your reports use your affiliate key.
Ask for programs that fit your site, and the agent gets a ranked list back.
Product tools return Clickwise links, never raw network tracking URLs, and the commission figures are what you earn.
For your own stack: product feeds in JSON or CSV, built from offers seen in the last forty-eight hours, and signed postbacks, HMAC-SHA256 with a timestamp, retried for up to eight attempts.
Machines can read the docs too: OpenAPI, llms.txt and an MCP manifest. Start at partners.clickwise.net/developers. The narration in this video is AI-generated.
https://partners.clickwise.net/api/v1/publisher/https://partners.clickwise.net/api/v1/mcpYour account (OAuth):
/api/v1/mcp/accountReady-made integrations
A paid WordPress plugin made by KeywordRush (not by Clickwise) with a Clickwise module: search the Clickwise catalog from the post editor and add products to your posts. Every product link is a Clickwise tracked link for your account.
Module setup guide (KeywordRush) →
Import a Clickwise product feed into WooCommerce with WP All Import.
WP All Import guide →
Workflow templates that call the Clickwise MCP server with the built-in MCP Client node.
Templates on GitHub →
Add Clickwise as an affiliate network in your tracker: the link parameter for its click ID and the postback URL to paste, step by step.
Tracker setup →
Pull your Clickwise sales and clicks into a reporting tool that reads many affiliate accounts by API. You connect it with your Reporting API token.
Reporting setup →
Do you use WordPress?
Content Egg Pro, a paid WordPress plugin made by KeywordRush (not by Clickwise), has a Clickwise module. You search the Clickwise product catalog from the post editor, add products to reviews and comparison tables, and every product link is a Clickwise tracked link created for your account. You need a Clickwise publisher account and its API key. Module setup guide (KeywordRush).
1. Get a sandbox key (no account)
We email you a link that reveals the key once. It lasts 14 days and covers the product search and lookup calls, over this REST API or the MCP tool search_products, at 10 requests per minute and 500 per day. Sandbox links are examples and do not track clicks or earn commission, and a search can return no products when no merchant is live in that market. It is a test key.
Agents and scripts can ask for one too (a human still opens the email):
curl -X POST https://partners.clickwise.net/api/v1/developers/sandbox-keys/ \
-H "Content-Type: application/json" -d '{"email": "you@example.com", "use_case": "price_comparison"}'
2. Quickstarts by use case
Start with a sandbox key (form above); sandbox links are examples and do not track clicks or earn commission. Which products a search returns depends on the merchants live in each market that day, and it can be none. To earn commission on the sales you drive, create a free publisher account and swap in your own key; the code does not change. Keep the key in an environment variable (CLICKWISE_API_KEY) on a server, never in browser code.
Search products by name in one market, then compare every offer for a GTIN (EAN/UPC), cheapest first. Each product carries a ready tracked link in url.
Turn API results into a CSV that WP All Import maps to WooCommerce external/affiliate products, then refresh it on a schedule.
Two remote MCP endpoints over Streamable HTTP. Public, no sign-in: programs, deals and catalog statistics. Your account, OAuth sign-in: your programs, reports, conversions, product search and tracked links (read and links only).
Your assistant searches products in one market, shows offers with a tracked link and earns commission on approved sales. You read the results per sub-ID through the API or receive a signed postback.
Post new merchant deals and coupon codes to Telegram, Discord or a newsletter. The deals API is public (no key); link each post to the deal page, or mint your own tracked link with your publisher key.
Paste the prompt into Lovable, Bolt or v0, add your key as a server-side secret named CLICKWISE_API_KEY, and publish. The store calls the API from a server function only.
Price comparison or GTIN shop
Search products by name in one market, then compare every offer for a GTIN (EAN/UPC), cheapest first. Each product carries a ready tracked link in url.
- Search: GET /api/v1/publisher/products/?q=<3-200 chars>&country=<ISO-2>. Follow next_offset for more.
- Compare: GET /api/v1/publisher/products/lookup/?gtins=<comma list>&country=<ISO-2> (1-100 ids and GTINs).
- Publish url exactly as returned (rel="sponsored nofollow"); refresh prices with lookup at least daily and hide items that are no longer returned.
curl
# 1. Products whose title contains "headphones" in the United Kingdom curl --get "https://partners.clickwise.net/api/v1/publisher/products/" \ -H "X-API-Key: $CLICKWISE_API_KEY" \ --data-urlencode "q=headphones" --data-urlencode "country=GB" --data-urlencode "limit=20" # 2. Every offer for the first product's GTIN (needs jq; keep GTINs as strings) GTIN=$(curl -s --get "https://partners.clickwise.net/api/v1/publisher/products/" -H "X-API-Key: $CLICKWISE_API_KEY" \ --data-urlencode "q=headphones" --data-urlencode "country=GB" --data-urlencode "limit=1" | jq -r '.products[0].gtin') curl --get "https://partners.clickwise.net/api/v1/publisher/products/lookup/" \ -H "X-API-Key: $CLICKWISE_API_KEY" \ --data-urlencode "gtins=$GTIN" --data-urlencode "country=GB"
JavaScript
// Node 18+ (built-in fetch). Run: CLICKWISE_API_KEY=... node compare.mjs
const BASE = "https://partners.clickwise.net/api/v1/publisher";
const headers = { "X-API-Key": process.env.CLICKWISE_API_KEY };
async function get(path) {
const res = await fetch(`${BASE}${path}`, { headers });
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`);
return body;
}
export async function compare(gtin, country = "GB") {
const { products } = await get(`/products/lookup/?gtins=${encodeURIComponent(gtin)}&country=${country}`);
return products
.filter((p) => p.availability === "in_stock")
.sort((a, b) => Number(a.price) - Number(b.price))
.map((p) => ({ merchant: p.merchant.name, price: `${p.price} ${p.currency}`, link: p.url }));
}
const { products } = await get("/products/?q=headphones&country=GB&limit=5");
console.table(products.map((p) => ({ title: p.title, gtin: p.gtin, price: p.price })));
if (products.length) console.table(await compare(products[0].gtin));Python
# pip install requests. Run: CLICKWISE_API_KEY=... python compare.py
import os
import requests
BASE = "https://partners.clickwise.net/api/v1/publisher"
HEADERS = {"X-API-Key": os.environ["CLICKWISE_API_KEY"]}
def get(path: str, **params) -> dict:
r = requests.get(f"{BASE}{path}", params=params, headers=HEADERS, timeout=20)
r.raise_for_status()
return r.json()
def compare(gtin: str, country: str = "GB") -> list[dict]:
offers = [p for p in get("/products/lookup/", gtins=gtin, country=country)["products"]
if p["availability"] == "in_stock"]
return sorted(offers, key=lambda p: float(p["price"]))
found = get("/products/", q="headphones", country="GB", limit=5)["products"]
for p in found:
print(p["title"], p["gtin"], p["price"], p["currency"])
if found:
for p in compare(found[0]["gtin"]):
print(p["merchant"]["name"], p["price"], p["currency"], p["url"])
WooCommerce / WordPress import (WP All Import)
Turn API results into a CSV that WP All Import maps to WooCommerce external/affiliate products, then refresh it on a schedule.
- Try the columns first with the public sample (no key, no links): https://partners.clickwise.net/api/v1/public/catalog-sample/?country=ES&format=csv
- Generate your import file with the script below (JavaScript or Python) from your searches; each row keeps its tracked link in product_url.
- WP All Import: New Items → WooCommerce Products → Product type "External/Affiliate product". Map title → Title, price → Regular Price, product_url → Product URL, button_text → Button text, image → Images (download from URL), sku → SKU, gtin → "GTIN, UPC, EAN or ISBN" (WooCommerce 9.2+). Unique identifier: {sku}.
- Schedule the import daily with "Delete products that are no longer present in your file" on, so expired offers disappear.
curl
# Sample columns (no key, no tracked links): map them before you have a key curl -o clickwise-sample.csv "https://partners.clickwise.net/api/v1/public/catalog-sample/?country=ES&format=csv" # Products with your tracked links, as JSON, one page of 100 curl --get "https://partners.clickwise.net/api/v1/publisher/products/" -H "X-API-Key: $CLICKWISE_API_KEY" \ --data-urlencode "q=headphones" --data-urlencode "country=GB" --data-urlencode "limit=100"
JavaScript
// Node 18+: writes clickwise-woocommerce.csv for WP All Import
import { writeFileSync } from "node:fs";
const BASE = "https://partners.clickwise.net/api/v1/publisher";
const headers = { "X-API-Key": process.env.CLICKWISE_API_KEY };
const queries = ["headphones", "keyboard"];
const country = "GB";
const cols = ["sku", "gtin", "title", "price", "currency", "image", "product_url", "button_text",
"merchant", "availability", "country", "updated_at"];
const csv = (v) => `"${String(v ?? "").replaceAll('"', '""')}"`;
const rows = new Map();
for (const q of queries) {
let offset = 0;
while (offset !== null && offset < 1000) {
const url = `${BASE}/products/?q=${encodeURIComponent(q)}&country=${country}&limit=100&offset=${offset}`;
const res = await fetch(url, { headers });
if (res.status === 429) { await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("Retry-After") || 60))); continue; }
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);
for (const p of body.products) {
rows.set(`${p.id}-${p.country}`, [`${p.id}-${p.country}`, p.gtin, p.title, p.price, p.currency, p.image,
p.url, `Buy at ${p.merchant.name}`, p.merchant.name, p.availability, p.country, p.updated_at]);
}
offset = body.next_offset;
}
}
writeFileSync("clickwise-woocommerce.csv", [cols, ...rows.values()].map((r) => r.map(csv).join(",")).join("\n"));
console.log(`${rows.size} products written`);Python
# pip install requests. Writes clickwise-woocommerce.csv for WP All Import
import csv
import os
import time
import requests
BASE = "https://partners.clickwise.net/api/v1/publisher"
HEADERS = {"X-API-Key": os.environ["CLICKWISE_API_KEY"]}
QUERIES, COUNTRY = ["headphones", "keyboard"], "GB"
COLS = ["sku", "gtin", "title", "price", "currency", "image", "product_url", "button_text",
"merchant", "availability", "country", "updated_at"]
rows = {}
for q in QUERIES:
offset = 0
while offset is not None and offset < 1000:
r = requests.get(f"{BASE}/products/", headers=HEADERS, timeout=30,
params={"q": q, "country": COUNTRY, "limit": 100, "offset": offset})
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 60)))
continue
r.raise_for_status()
body = r.json()
for p in body["products"]:
sku = f'{p["id"]}-{p["country"]}'
rows[sku] = [sku, p["gtin"], p["title"], p["price"], p["currency"], p["image"], p["url"],
f'Buy at {p["merchant"]["name"]}', p["merchant"]["name"], p["availability"],
p["country"], p["updated_at"]]
offset = body["next_offset"]
with open("clickwise-woocommerce.csv", "w", newline="", encoding="utf-8") as fh:
writer = csv.writer(fh)
writer.writerow(COLS)
writer.writerows(rows.values())
print(len(rows), "products written")PHP (WordPress)
<?php
// wp-config.php: define('CLICKWISE_API_KEY', '...'); Never print the key or put it in a URL.
// Refresh price and stock of imported products by GTIN, once a day, server side.
add_action('clickwise_refresh_prices', function () {
$products = wc_get_products(['limit' => 100, 'type' => 'external', 'return' => 'objects']);
$by_gtin = [];
foreach ($products as $product) {
$gtin = $product->get_global_unique_id();
if ($gtin) { $by_gtin[$gtin] = $product; }
}
if (!$by_gtin) { return; }
$url = add_query_arg(['gtins' => implode(',', array_keys($by_gtin)), 'country' => 'DE'],
'https://partners.clickwise.net/api/v1/publisher/products/lookup/');
$response = wp_remote_get($url, ['timeout' => 20, 'headers' => ['X-API-Key' => CLICKWISE_API_KEY]]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return; }
$seen = [];
foreach (json_decode(wp_remote_retrieve_body($response), true)['products'] as $p) {
$product = $by_gtin[$p['gtin']] ?? null;
if (!$product || isset($seen[$p['gtin']])) { continue; }
$seen[$p['gtin']] = true;
$product->set_regular_price($p['price']);
$product->set_product_url($p['url']);
$product->set_status($p['availability'] === 'in_stock' ? 'publish' : 'draft');
$product->save();
}
foreach ($by_gtin as $gtin => $product) { // no longer offered: hide it
if (!isset($seen[$gtin])) { $product->set_status('draft'); $product->save(); }
}
});
if (!wp_next_scheduled('clickwise_refresh_prices')) {
wp_schedule_event(time(), 'daily', 'clickwise_refresh_prices');
}
AI agent over MCP (Claude, Cursor, ChatGPT)
Two remote MCP endpoints over Streamable HTTP. Public, no sign-in: programs, deals and catalog statistics. Your account, OAuth sign-in: your programs, reports, conversions, product search and tracked links (read and links only).
- Public tools, no key: https://partners.clickwise.net/api/v1/mcp
- Your account: https://partners.clickwise.net/api/v1/mcp/account (the client opens Clickwise sign-in and asks you to approve read and links access; revoke it any time at https://partners.clickwise.net/oauth/connections/).
- Claude (claude.ai, Desktop): Settings → Connectors → Add custom connector → paste the URL.
- Claude Code: claude mcp add --transport http clickwise https://partners.clickwise.net/api/v1/mcp/account, then run /mcp to sign in.
- Cursor: add the server to ~/.cursor/mcp.json (below); Cursor starts the sign-in on first use.
- ChatGPT: enable developer mode, create a connector with the /api/v1/mcp/account URL and OAuth authentication.
- Headless agents can send a key instead: header X-API-Key on https://partners.clickwise.net/api/v1/mcp. A sandbox key covers search_products (by text or GTIN); every other account tool needs your publisher key.
curl
# Public tool call over plain JSON-RPC (no key)
curl -s "https://partners.clickwise.net/api/v1/mcp" \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"find_affiliate_programs","arguments":{"topic":"headphones","country":"DE","limit":5}}}'
# Cursor: ~/.cursor/mcp.json
# {"mcpServers": {"clickwise": {"url": "https://partners.clickwise.net/api/v1/mcp/account"}}}
# Claude Code with a key instead of sign-in
claude mcp add --transport http clickwise "https://partners.clickwise.net/api/v1/mcp" --header "X-API-Key: $CLICKWISE_API_KEY"JavaScript
// npm install @modelcontextprotocol/sdk
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "my-shopping-agent", version: "1.0.0" });
await client.connect(new StreamableHTTPClientTransport(new URL("https://partners.clickwise.net/api/v1/mcp")));
const { tools } = await client.listTools();
console.log(tools.map((t) => t.name));
const stats = await client.callTool({ name: "get_catalog_stats", arguments: {} });
console.log(stats.content[0].text);
await client.close();Python
# pip install mcp
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
async with streamablehttp_client("https://partners.clickwise.net/api/v1/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools])
result = await session.call_tool("find_affiliate_programs",
{"topic": "running shoes", "country": "ES", "limit": 5})
print(result.content[0].text)
asyncio.run(main())
Earn commission from your AI assistant
Your assistant searches products in one market, shows offers with a tracked link and earns commission on approved sales. You read the results per sub-ID through the API or receive a signed postback.
Transcript
Want your AI assistant to recommend products and earn commission? Start with a free sandbox key.
Search products in one market. Each offer comes with its link.
Show the link exactly as it comes, and let your user open it. Don't open it yourself: every request counts as a click.
Sandbox links are examples: they don't track clicks or earn commission. To earn, create a publisher account and use its key.
With a publisher key, read your sales by sub-ID, or receive a signed postback.
Commission is paid on approved sales: the network or the merchant approves each sale. Full guide at partners.clickwise.net/developers.
- Try it with a sandbox key (form above): search with q and country, for example q=headphones and country=GB. Sandbox links are examples and do not track clicks or earn commission, and a sandbox search returns at most 10 offers.
- To earn, create a publisher account and use its key. List the programs open to you with GET /api/v1/publisher/programs/joinable/ and apply with POST /api/v1/publisher/programs/apply/; an application is approved or goes to review. A search returns offers only from the programs you are approved in.
- Search when the user asks for something: GET /api/v1/publisher/products/?q=<3-200 chars>&country=<ISO-2>, or the MCP tool search_products. Each offer has url, its tracked link, and rules, the program's rules for that offer: follow them.
- Tag each conversation: add subid=<your id: up to 36 characters, letters, digits, _ or -, no personal data> to the search. The links come back with it, and a sale made through one of those links reports it in your conversions.
- Show the url exactly as returned and let the user open it. Never request the url yourself or prefetch it: every request counts as a click. Tell your users that the links are affiliate links.
- Read results with GET /api/v1/publisher/conversions/ (per-sub-ID totals in by_subid) or GET /api/v1/publisher/reports/?group_by=subid. Or set a postback URL in the portal: each conversion arrives as a signed request with subid, click_id, transaction_id, order_ref, event and status. If you choose event types for it, only those arrive, and choosing sale.approved also brings sale.commission_validated. Sales from network programs can show as pending validation, with no commission amount until the sale is validated; the amount then arrives in a sale.commission_validated postback with the same transaction_id.
- Verify every postback: for a POST postback, X-CW-Signature is the hex HMAC-SHA256 of X-CW-Timestamp, a dot and the raw request body, keyed with the postback secret from your portal. Send a test event with POST /api/v1/publisher/postback/test/.
- Commission is paid on approved sales; the network or the merchant approves each sale.
curl
# 1. Search in one market (sandbox key or your publisher key)
curl --get "https://partners.clickwise.net/api/v1/publisher/products/" \
-H "X-API-Key: $CLICKWISE_API_KEY" \
--data-urlencode "q=headphones" --data-urlencode "country=GB" --data-urlencode "limit=5" \
--data-urlencode "subid=chat-42"
# 2. Programs you can apply to, and an application (publisher key)
curl "https://partners.clickwise.net/api/v1/publisher/programs/joinable/" -H "X-API-Key: $CLICKWISE_API_KEY"
curl -X POST "https://partners.clickwise.net/api/v1/publisher/programs/apply/" -H "X-API-Key: $CLICKWISE_API_KEY" \
-H "Content-Type: application/json" -d '{"program_id": 123}'
# 3. Results per sub-ID
curl --get "https://partners.clickwise.net/api/v1/publisher/reports/" -H "X-API-Key: $CLICKWISE_API_KEY" \
--data-urlencode "group_by=subid"JavaScript
// Node 18+. Verify a Clickwise postback before trusting it.
import crypto from "node:crypto";
export function verifyPostback(rawBody, timestamp, signature, secret) {
const expected = crypto.createHmac("sha256", secret)
.update(`${timestamp}.`).update(rawBody).digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(String(signature || ""), "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// In your handler: verifyPostback(req.rawBody, req.headers["x-cw-timestamp"],
// req.headers["x-cw-signature"], process.env.CLICKWISE_POSTBACK_SECRET)Python
# Verify a Clickwise postback before trusting it.
import hashlib
import hmac
import os
def verify_postback(raw_body: bytes, timestamp: str, signature: str) -> bool:
secret = os.environ["CLICKWISE_POSTBACK_SECRET"].encode()
expected = hmac.new(secret, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
Coupon and deals bot
Post new merchant deals and coupon codes to Telegram, Discord or a newsletter. The deals API is public (no key); link each post to the deal page, or mint your own tracked link with your publisher key.
- Poll GET /api/v1/deals/?lang=en&page_size=50 (lang: en, es, pt, fr, de, it); results are newest first.
- Remember the ids you already posted; skip deals whose valid_until has passed.
- Link to https://partners.clickwise.net + url (the public deal page). With a publisher key, POST /api/v1/publisher/links/ with subid "telegram" to earn and see clicks per channel in /reports/?group_by=subid.
- RSS alternative: https://partners.clickwise.net/api/v1/deals/feed.en.xml
curl
curl "https://partners.clickwise.net/api/v1/deals/?lang=en&page_size=20"
# Your own tracked link for a deal's merchant (publisher key, earns commission)
curl -X POST "https://partners.clickwise.net/api/v1/publisher/links/" -H "X-API-Key: $CLICKWISE_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: deal-123-telegram" \
-d '{"campaign_id": 123, "subid": "telegram"}'JavaScript
// Node 18+: post new deals to a Discord channel webhook every run (cron it)
import { existsSync, readFileSync, writeFileSync } from "node:fs";
const SEEN = "seen-deals.json";
const seen = new Set(existsSync(SEEN) ? JSON.parse(readFileSync(SEEN, "utf8")) : []);
const { results } = await (await fetch("https://partners.clickwise.net/api/v1/deals/?lang=en&page_size=50")).json();
for (const d of results.reverse()) {
if (seen.has(d.id) || (d.valid_until && new Date(d.valid_until) < new Date())) continue;
const off = d.discount_pct ? `${d.discount_pct}% off` : d.discount_amount ? `${d.discount_amount} ${d.discount_currency} off` : "";
const code = d.code ? ` · code **${d.code}**` : "";
await fetch(process.env.DISCORD_WEBHOOK_URL, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: `**${d.merchant_name}**: ${d.title} ${off}${code}\nhttps://partners.clickwise.net${d.url}` }),
});
seen.add(d.id);
}
writeFileSync(SEEN, JSON.stringify([...seen]));Python
# pip install requests. Post new deals to a Telegram channel (cron it)
import json
import os
from datetime import datetime, timezone
from pathlib import Path
import requests
SEEN = Path("seen-deals.json")
seen = set(json.loads(SEEN.read_text())) if SEEN.exists() else set()
deals = requests.get("https://partners.clickwise.net/api/v1/deals/", params={"lang": "en", "page_size": 50}, timeout=20).json()["results"]
now = datetime.now(timezone.utc)
for d in reversed(deals):
if d["id"] in seen or (d["valid_until"] and datetime.fromisoformat(d["valid_until"]) < now):
continue
off = f'{d["discount_pct"]}% off' if d["discount_pct"] else ""
code = f' · code {d["code"]}' if d["code"] else ""
text = f'{d["merchant_name"]}: {d["title"]} {off}{code}\nhttps://partners.clickwise.net{d["url"]}'
requests.post(f'https://api.telegram.org/bot{os.environ["TELEGRAM_TOKEN"]}/sendMessage',
json={"chat_id": os.environ["TELEGRAM_CHAT_ID"], "text": text}, timeout=20)
seen.add(d["id"])
SEEN.write_text(json.dumps(sorted(seen)))
Affiliate store built with Lovable, Bolt or v0
Paste the prompt into Lovable, Bolt or v0, add your key as a server-side secret named CLICKWISE_API_KEY, and publish. The store calls the API from a server function only.
- Get a sandbox key (above) and add it as a secret: Lovable → Supabase secret, Bolt → .env on the server, v0/Vercel → Environment Variable. Never in client code.
- Paste the prompt below. It asks for a server function (Supabase Edge Function, Next.js Route Handler or a server route) that proxies the products API and caches for 10 minutes.
- Publish, then swap the sandbox key for your own publisher key so purchases pay you.
Prompt to paste into Lovable, Bolt or v0
Build a fast, mobile-first affiliate product store called "My Deals Store" that lists products from the Clickwise Publisher Products API.
Data source (server side only):
- Base URL: https://partners.clickwise.net/api/v1/publisher/
- Send the header X-API-Key with the value of the secret CLICKWISE_API_KEY. Never expose the key in browser code, URLs or logs: call the API only from a server function (Supabase Edge Function, Next.js Route Handler or server route) and have the frontend call that function.
- Search: GET products/?q=<text>&country=<ISO-2 such as DE, ES, GB, FR, IT, NL>&limit=24&offset=<n>. q needs 3-200 characters. Response: {products: [{id, gtin, title, price, currency, image, url, merchant: {id, name}, availability, country, updated_at}], count, next_offset}. Use next_offset for "Load more" (null means no more).
- Refresh: GET products/lookup/?gtins=<comma-separated GTINs>&country=<ISO-2> (up to 100) returns current price and stock; hide products missing from the answer.
- Errors are JSON {"error": {"code", "message", "status"}}. On 429 wait the Retry-After seconds; on 503 retry with backoff. Cache successful answers server side for 10 minutes.
Pages and UI:
- Home: a search box, a country selector and category chips (headphones, keyboards, monitors, books) that run searches.
- Product grid: image, title, merchant name, price formatted with its currency, an availability badge and a "View deal" button.
- The button opens product.url exactly as returned (it is the tracked link) in a new tab with rel="sponsored nofollow noopener". Never rewrite, shorten or strip that URL.
- Product detail drawer with the GTIN and "last updated" time.
- Footer on every page: "We earn a commission from purchases made through links on this site, at no extra cost to you."
- Loading, empty and error states; responsive, accessible, lazy-loaded images.
Show only prices the API returns; never invent or store prices.
curl
# Check your key before you build (expects a JSON list of products) curl --get "https://partners.clickwise.net/api/v1/publisher/products/" -H "X-API-Key: $CLICKWISE_API_KEY" \ --data-urlencode "q=headphones" --data-urlencode "country=GB" --data-urlencode "limit=3"
JavaScript
// Next.js Route Handler: app/api/products/route.js (v0 / Vercel). The key stays on the server.
export const revalidate = 600;
export async function GET(request) {
const { searchParams } = new URL(request.url);
const q = (searchParams.get("q") || "headphones").slice(0, 200);
const country = /^[A-Z]{2}$/.test(searchParams.get("country") || "") ? searchParams.get("country") : "GB";
const offset = Number(searchParams.get("offset") || 0);
const upstream = await fetch(
`https://partners.clickwise.net/api/v1/publisher/products/?q=${encodeURIComponent(q)}&country=${country}&limit=24&offset=${offset}`,
{ headers: { "X-API-Key": process.env.CLICKWISE_API_KEY }, next: { revalidate: 600 } },
);
return new Response(await upstream.text(), {
status: upstream.status,
headers: { "Content-Type": "application/json", "Cache-Control": "s-maxage=600" },
});
}Python
# pip install fastapi uvicorn httpx. Minimal proxy for any frontend: uvicorn store:app
import os
import httpx
from fastapi import FastAPI, Query
app = FastAPI()
BASE = "https://partners.clickwise.net/api/v1/publisher"
@app.get("/api/products")
async def products(q: str = Query("headphones", min_length=3, max_length=200),
country: str = Query("GB", pattern="^[A-Z]{2}$"), offset: int = 0):
async with httpx.AsyncClient(timeout=20) as client:
r = await client.get(f"{BASE}/products/", headers={"X-API-Key": os.environ["CLICKWISE_API_KEY"]},
params={"q": q, "country": country, "limit": 24, "offset": offset})
return r.json()
3. Your own account in four calls
# Who am I, what can this key do, what are my limits?
curl https://partners.clickwise.net/api/v1/publisher/me/ -H "X-API-Key: $CLICKWISE_PUBLISHER_KEY"
# Programs I can promote
curl https://partners.clickwise.net/api/v1/publisher/programs/ -H "X-API-Key: $CLICKWISE_PUBLISHER_KEY"
# A tracking link to a product page, tagged with my sub-ID
curl -X POST https://partners.clickwise.net/api/v1/publisher/links/ \
-H "X-API-Key: $CLICKWISE_PUBLISHER_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: post-4711-link" \
-d '{"campaign_id": 123, "subid": "post_4711", "landing_url": "https://merchant.example/p/1"}'
# Commission by day for the last 30 days
curl "https://partners.clickwise.net/api/v1/publisher/reports/?group_by=day" -H "X-API-Key: $CLICKWISE_PUBLISHER_KEY"
4. Use your publisher API key
Use the API key from your Clickwise publisher account—the same credential used by the affiliate MCP tools. Send either X-API-Key: YOUR_PUBLISHER_KEY or Authorization: Bearer YOUR_PUBLISHER_KEY over HTTPS. Keep this key on your server (for example, in the WordPress plugin’s server configuration). Do not embed it in a public page, URL or browser script. Portal session cookies and operator keys do not authenticate this API. Product search and lookup also need product feed access on your account.
To try the API first, get a sandbox key by email. A demo key works only with this Publisher Products API (search and lookup) and the MCP tool search_products: every other Clickwise API, including programs, links, reports and postbacks, and the other MCP tools answer 403 to it, and a demo account never applies to programs. A sandbox key has the same scope, and its responses carry X-Clickwise-Sandbox: 1. Integration questions: partnerships@clickwise.net, or talk to our AI agent 24/7 at +1 646 362 1368 (US) or in your browser.
A key acts only for its own account. It can create your links, apply to programs on your behalf and configure your postback; it can never approve applications, commissions or payouts, and it has no administrative access.
5. Conventions
| Topic | Rule |
|---|---|
| Rate limits | Fixed one-minute windows per publisher account (a rotated key keeps its budget), plus a per-address limit before the key is checked. Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (seconds), RateLimit-Policy and the X-RateLimit-* equivalents; a 429 adds Retry-After. GET /me/ returns your current limits. |
| Request IDs | Every response has X-Request-Id. Send your own (8–128 characters of A-Z a-z 0-9 . _ : -) to correlate logs; quote it when you contact support. |
| Errors | One shape: {"detail": "…", "error": {"code": "…", "message": "…", "status": 403, "request_id": "…"}}. Branch on error.code (for example not_authenticated, api_key_scope, throttled, invalid_request, not_found, idempotency_key_reused). |
| Idempotency | POST requests accept Idempotency-Key. A retry with the same key and body within 24 h replays the first response (Idempotent-Replayed: true); the same key with a different body is a 409. Links are also idempotent by campaign and sub-ID. |
| Pagination | Products: limit/offset, follow next_offset. Lists: page/page_size with total and pages. |
| Versioning | Changes inside v1 are additive (new fields, new endpoints). A breaking change ships as a new version, announced in advance with Deprecation and Sunset headers. X-Clickwise-Api-Version reports the current minor version. |
6. Endpoints
| Method & path | Purpose |
|---|---|
GET /me/ | Account, access and limits for this key. |
GET /products/ · GET /products/lookup/ | Search GTIN products; refresh price and stock by id or GTIN. |
GET /offers/ | Pilot: authorized coupons and deals for one country (country, limit, page). When Clickwise enables access for your account, generate your dedicated offers credential once in the Offers feed section of your publisher home. You can rotate or revoke it there. Never include it in a URL or email. Requires (Authorization: Bearer cw_off_…), not your publisher key. |
GET /offers.csv | Pilot: the same authorized offers for one country (country only) as one CSV file with a fixed header row. Same dedicated offers credential as /offers/ (Authorization: Bearer cw_off_…), not your publisher key. |
GET /trending-opportunities/ | Trend Radar for an explicitly subscribed publisher: JSON or CSV, optional country, channel and limit (maximum 50). Requires enabled Trend Radar access. Only released, current opportunities; a tracked URL requires an approved program assignment and current eligibility. Other items carry status and reason without a link. Maximum 60 requests per minute. |
GET /programs/ · GET /programs/joinable/ | Programs you may promote (add include_available=true for programs you can apply to), with link readiness and the commission you earn. |
POST /programs/apply/ | Apply with program_id or campaign_id; the answer is approved or pending review. |
GET|POST /links/ · POST /links/batch/ | List or create tracking links; up to 50 per batch, each item with its own status. |
GET /conversions/ | Your conversions with status, commission, currency and sub-ID, plus a sub-ID rollup. Sales from network programs can show as pending validation, with no commission amount until the sale is validated. The validated row keeps the same transaction_id. |
GET /payment-status/ | Your approved commission not yet paid, commission pending review, the next payment estimate with its threshold, and your payment method as its type (a bank account shows only its last 4 digits). Read-only; never returns account numbers, PayPal addresses, postal or tax data. |
GET /reports/ | Conversions and commission by day, week, month, subid, campaign or status. |
GET /clicks/ | Your clicks on Clickwise redirects, filterable by sub-ID. |
GET /postback/ · POST /postback/test/ · GET /postback/deliveries/ | Conversion postback configuration, test event and delivery log. The URL and the signing secret are set in the portal (Settings > Postback), never with an API key. |
7. Search products
GET /api/v1/publisher/products/
| Parameter | Meaning |
|---|---|
q · required | Case-insensitive phrase contained in the product title. 3–200 characters. |
country | Optional ISO alpha-2 market, such as ES, DE or GB. UK is accepted as an alias for GB. Omit it to search the markets enabled for your product-feed access; an account without a market list gets 400 and must send country. |
merchant | Optional positive integer: the Clickwise merchant/campaign ID returned in merchant.id. An unapproved merchant yields no products. |
limit | 1–100 products; default 20. |
offset | 0–10000; default 0. Follow next_offset, keeping all other filters unchanged. |
group | Optional 1: one result per group_id (the sizes of one model and colour) — the cheapest in-stock variant, with sizes listing the sizes in stock; total then counts groups. Without it, every variant is its own result. |
min_price · max_price | Optional price range, inclusive, in each product’s own currency (the currency field of the result; prices are never converted). Numbers from 0 with up to 4 decimals, such as 19.99; either bound alone works. min_price greater than max_price returns 400. total counts only the products in the range. |
currency | Optional ISO 4217 code, such as EUR or GBP: only products priced in that currency. With a price range, the range applies to that currency. |
curl --get 'https://partners.clickwise.net/api/v1/publisher/products/' \ -H "X-API-Key: $CLICKWISE_PUBLISHER_KEY" \ --data-urlencode 'q=headphones' \ --data-urlencode 'country=GB' \ --data-urlencode 'min_price=10' \ --data-urlencode 'max_price=200' \ --data-urlencode 'limit=20'
Each product carries group_id, shared by the sizes of one model and colour: the feed’s item_group_id when it has one, otherwise brand + title without the size + colour, normalised (a heuristic). merchant.domain is the merchant’s public website domain, such as tooled-up.com (for store logos and “Buy at” buttons; it is not a link), or null.
Search returns fresh, in-stock products only, ordered by stable product ID. Pagination reflects the current catalog; changing inventory or permissions can move items between pages.
Refresh products in existing posts
GET /api/v1/publisher/products/lookup/ — supply ids, gtins, or both: comma-separated lists with 1–100 identifiers combined. Keep GTINs as strings to preserve leading zeroes; checksums are validated.
curl --get 'https://partners.clickwise.net/api/v1/publisher/products/lookup/' \ -H "Authorization: Bearer $CLICKWISE_PUBLISHER_KEY" \ --data-urlencode 'ids=12345,12346' \ --data-urlencode 'gtins=4006381333931,5901234123457' \ --data-urlencode 'country=DE'
Lookup includes fresh out-of-stock and preorder/backorder products. Missing, expired, removed or unauthorized items are omitted identically; hide stale offers rather than keeping an old price.
{
"products": [{
"id": "12345", "gtin": "4006381333931", "title": "Example running shoes",
"price": "79.9500", "currency": "EUR", "image": "https://merchant.example/images/shoes.jpg",
"url": "https://partners.clickwise.net/dl/gfEXAMPLE/?sub_id=feed_42_es",
"merchant": {"id": 123, "name": "Example Store", "domain": "example-store.com"},
"availability": "in_stock", "country": "ES", "updated_at": "2026-09-28T08:00:00+00:00",
"brand": "Example", "category": "Running shoes", "regular_price": "99.9500",
"description": "Lightweight running shoe with a breathable mesh upper.", "size": "42", "color": "Blue",
"group_id": "g3f1c2a9b7d4e6f80"
}],
"count": 1, "total": 1, "total_exact": true, "limit": 20, "offset": 0, "next_offset": null, "freshness_hours": 48
}
Illustrative response; identifiers and links are placeholders. price is a decimal string. brand, category, regular_price (the merchant's list price, only when above price), description (plain text, up to 500 characters), size and color are null when the merchant's feed has none; each size of a product is its own result. q matches all its words in any order, most relevant first. total counts search matches before per-product link checks, up to 1000 (total_exact is false at the cap); count is the number on this page. Use url as the product link without replacing its attribution. The product endpoints allow 60 requests per minute per account.
8. Tracking links, one or many
POST /links/ takes campaign_id (or program_id), an optional subid and an optional landing_url on the merchant’s own domain, and returns cw_link — the link to publish. POST /links/batch/ takes up to 50 such objects; one bad item never fails the others.
curl -X POST https://partners.clickwise.net/api/v1/publisher/links/batch/ \
-H "X-API-Key: $CLICKWISE_PUBLISHER_KEY" -H "Content-Type: application/json" \
-d '{"links": [{"campaign_id": 123, "subid": "post_1"}, {"program_id": 456, "subid": "post_2"}]}'
{"data": [{"index": 0, "status": 201, "link": {"cw_link": "https://partners.clickwise.net/dl/…", "subid": "post_1", "created": true}},
{"index": 1, "status": 404, "error": {"code": "not_found", "message": "…", "status": 404, "request_id": "…"}}],
"created": 1, "existing": 0, "failed": 1}
9. Conversions and reports
GET /reports/?group_by=day&date_from=2026-09-01&date_to=2026-09-29 returns one row per period (empty periods included) or per sub-ID, campaign or status, with conversions, pending, approved, declined, commission per currency and USD totals. Commission is what you earn; declined sales are counted but never paid, so they are excluded from commission. Rows without a USD rate are counted in unconverted instead of being mixed into USD totals. Ranges are up to 366 days (default: last 30). GET /conversions/ lists the individual sales (default: last 90 days).
Sales from network programs can show as pending validation, with no commission amount until the sale is validated.
10. Conversion postbacks (webhooks)
Set the postback in the portal (Settings > Postback): an https url containing macros — {subid}, {click_id}, {transaction_id}, {order_ref}, {sale_amount}, {commission}, {currency}, {status}, {event}, {campaign_id}, {date} (when the sale happened, ISO 8601 UTC such as 2026-10-05T14:03:09Z; not when the postback is sent) — and http_method GET or POST. Events (what you subscribe to, and the X-CW-Event header): sale.created, sale.approved, sale.declined, sale.reversed, sale.commission_validated. The {event} macro (and the event field of a POST body) carries the short value: created, approved, declined, reversed or commission_validated. The first save returns the signing secret once; rotate it with POST /postback/secret/. Failed deliveries are retried with exponential backoff (up to 8 attempts, capped at 6 h).
Sales from network programs can show as pending validation, with no commission amount until the sale is validated; the amount then arrives in a sale.commission_validated postback with the same transaction_id.
Using a tracker? Set up Clickwise as an affiliate network in Voluum, RedTrack, Binom, Keitaro, BeMob or ClickMagick: the tracker sends its click ID in sub_id and gets it back as {subid}.
Each delivery carries X-CW-Event, X-CW-Delivery, X-CW-Timestamp and X-CW-Signature = hex HMAC-SHA256 of "<X-CW-Timestamp>." + message, where the message is the full URL for GET and the raw JSON body for POST.
import hashlib, hmac, time
def valid_postback(secret: str, timestamp: str, message: bytes, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 300: # reject replays older than 5 minutes
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
11. MCP server for AI agents
Remote MCP over Streamable HTTP at https://partners.clickwise.net/api/v1/mcp. It speaks protocol 2026-07-28 (per-request metadata, server/discover) and the 2025-11-25, 2025-06-18 and 2025-03-26 handshake revisions. Public tools find programs, deals and catalog statistics without a key. With your account, list_my_programs, create_tracking_links, get_performance_report, list_conversions, search_products and get_product_feed act on your own account through this same API, limits included; apply_to_program needs your API key. A sandbox key also works for search_products (by text or GTIN); sandbox links are examples and do not track clicks or earn commission. The other account tools need your publisher key.
Sign in with OAuth (Claude, ChatGPT, Cursor)
Add https://partners.clickwise.net/api/v1/mcp/account as a custom connector. The client registers itself, opens a Clickwise page where you sign in and approve, and receives a token for your account only. Scopes: affiliate:read (programs, reports, conversions) and affiliate:links (tracked links, product search). An OAuth connection never applies to programs, changes payout or postback settings, or approves anything. See and disconnect apps at Connected apps. Metadata: protected resource (RFC 9728), authorization server (RFC 8414); PKCE S256 and dynamic client registration.
Or send your API key
{
"mcpServers": {
"clickwise": {
"url": "https://partners.clickwise.net/api/v1/mcp",
"headers": {"X-API-Key": "YOUR_PUBLISHER_KEY"}
}
}
}
Discovery: /.well-known/mcp.json and the MCP server card (SEP-2127 draft).
12. HasOffers/TUNE-compatible endpoint
Tools that already speak the HasOffers Affiliate API v3 work by changing the base URL to https://partners.clickwise.net/Apiv3/json and using your Clickwise key as api_key (or, better, as the X-API-Key header, which keeps it out of URLs and logs). Supported: Affiliate_Offer findAll, findMyOffers, findMyApprovedOffers, findById, generateTrackingLink; Affiliate_Report getStats (groups by date, week, month, Stat.affiliate_info1 or Stat.offer_id) and getConversions. Responses use the HasOffers envelope; other methods answer status: -1 with the supported list.
curl "https://partners.clickwise.net/Apiv3/json?Target=Affiliate_Offer&Method=generateTrackingLink&offer_id=123¶ms%5Baff_sub%5D=post_1" \ -H "X-API-Key: $CLICKWISE_PUBLISHER_KEY"
13. Limits & errors
Default limits per publisher account: 120 requests per minute across the API, 30 writes per minute for applications, 300 link units per minute (one per batch item), and 60 requests per minute for product search and lookup. Response pages contain at most 100 products. Catalog rows must have been seen within 48 hours. Responses use Cache-Control: private, no-store.
| Status | What to do |
|---|---|
| 200/201 | Read the body. For products follow next_offset until null; an empty array is a valid result. |
| 400 | Correct invalid, unknown or repeated parameters, invalid GTINs, or exceeded bounds. Send country if the response asks for it. |
| 401 | Supply an active publisher API key. A revoked key or inactive account cannot authenticate. |
| 403 | The key is not allowed here (for example a demo key outside products, or product feed access not active). |
| 404 | The program or campaign is not available to your account (not assigned, paused or closed). |
| 405 | Use the documented method. |
| 409 | The Idempotency-Key was already used with a different body. |
| 429 | Wait for the Retry-After response header (seconds), then retry with backoff. |
| 503 | Temporary dependency failure or a query over its time budget. On 503, retry once after the Retry-After header (seconds); a broad first search can take a few seconds while the catalog warms up. No untracked fallback links are returned. |
{"detail": "Product feed access is not active for this publisher.",
"error": {"code": "permission_denied", "message": "Product feed access is not active for this publisher.", "status": 403, "request_id": "4f1c…"}}
Keys can be disabled by Clickwise; revocation applies to the next request. Demo accounts use real scoped catalog data but are marked as test and nonpayable accounts and never earn commission. Do not use a demo key for revenue traffic.
14. Offer feed and programme lists (coupon and deal sites)
For partners that list codes and deals. Your feed key and its URLs are in your dashboard, in Product feeds > Your offer feed. The feed key is personal: keep it on your server. The feed and the programme list include only the programmes whose terms allow distribution through your site or network.
| Method & path | Purpose |
|---|---|
GET /api/v1/offer-feed/{feed_key}.json · .csv | Coupons API: the live codes, and the deals without a code as link offers, that the programmes allow on coupon sites. Body {"offers": [...], "google_ads_templates": false, "coupon_sync_at": "…"}. coupon_sync_at and the X-Coupon-Sync-At header say when a network sync last saw a live code; X-Feed-Generated-At says when the response was built. |
GET /api/v1/offer-feed/{feed_key}/programs.json | All programmes API: every programme your feed key may distribute, approved for you or not. Paginated. |
GET /api/v1/affiliate/approved-programs/ | Approved programmes API: the programmes approved for your account, with your link. Authenticate with your publisher API key (section 4). format=json (default) or format=csv; in CSV the page data comes in the X-Count, X-Page, X-Page-Size, X-Next-Page and X-Previous-Page headers. Paginated. |
Parameters for the three: country (ISO 3166-1 alpha-2) and updated_since (ISO 8601 with a time zone). The programme lists also take page and page_size (default 100, maximum 200) and answer {"count", "page", "page_size", "next_page", "previous_page", "results"}; follow next_page until it is null. Responses carry a strong ETag: send it back in If-None-Match to get a 304 when nothing changed.
Programme fields
| Field | Meaning |
|---|---|
name | Programme (brand) name. |
domain | The brand's site. |
campaign_id | Clickwise campaign id, the same one used in links and reports. |
countries_iso2 | Markets of the programme as ISO 3166-1 alpha-2 codes; empty when the programme publishes no market list. |
category | List of categories. |
commission | What you earn, as a label (for example 30%); when the rate varies by product, the programme's rules say so. |
logo_url | Brand logo. |
extensions | prohibited or no_written_prohibition, for browser extensions. |
coupons_allowed | true when the programme's terms allow coupon sites. |
universal_link_format | The programme's link with {affiliate_id} as its only variable. |
updated_at | ISO 8601: the last change of the programme, its terms or its live codes. |
universal_link | Approved programmes only: your link, ready to use. |
needs_public_id | Approved programmes only: true when your account has no public id yet, so universal_link cannot be built; contact us. |
Offer fields (Coupons API)
| Field | Meaning |
|---|---|
brand, domain, campaign | Brand name, its site and the Clickwise campaign id. |
countries, countries_iso2 | Markets as published by the programme, and as ISO 3166-1 alpha-2 codes. |
category, commission, logo_url, extensions, updated_at | As in the programme fields. |
coupons | List of codes: code, description, valid_until (ISO 8601 or null), fetched_at (when it was last seen at the source), alcance, entidad and reglas (the rule ids that apply). Empty for a deal without a code. |
rules | The programme's rules for publishers, in English. Read them before you publish. |
link_template | The offer link with {affiliate_id} as its only variable. |
universal_link | The programme's link. |
request_url | Where to request the programme in your dashboard if it is not approved for you yet. |
valid_until, google_ads_templates | Reserved at offer level: null and false. The validity of each code is in coupons[].valid_until. |