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.
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. Responses use example tracked links, and a search can return no products when no merchant is live in that market. It is a test key and never earns commission.
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: example links, never earns). 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).
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 "sander" in the United Kingdom curl --get "https://partners.clickwise.net/api/v1/publisher/products/" \ -H "X-API-Key: $CLICKWISE_API_KEY" \ --data-urlencode "q=sander" --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=sander" --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=sander&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="sander", 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=drill" --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 = ["drill", "bosch"];
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 = ["drill", "bosch"], "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())
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 (drills, saws, sanders, garden tools) 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=sander" --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") || "sander").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("sander", 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), +44 7576 559749 (UK) 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 /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. |
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=running shoes' \ --data-urlencode 'country=ES' \ --data-urlencode 'min_price=40' \ --data-urlencode 'max_price=120' \ --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).
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. The {event} macro (and the event field of a POST body) carries the short value: created, approved, declined or reversed. 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).
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), with example links that never earn; 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 and tracked links but are marked as test and nonpayable accounts. Do not use a demo key for revenue traffic.