Cloaker.Net

Cloaker Decision Engine API Integration

Complete guide to integrating the Cloaker decision API — authentication, endpoints, execution methods, reverse proxy behavior, and production-ready examples for Cloudflare Workers, Vercel Edge, AWS Lambda, Fastly Compute, PHP, WordPress, Node.js, and Python.

Cloaker Decision Engine API Integration

Cloaker analyzes every incoming visit — IP, network, device, headers, geolocation, campaign rules — and returns a decision telling your server exactly what to do with that visitor: redirect them, proxy another site transparently, render an iframe, and so on.

Your server (an edge function, a PHP script, a WordPress plugin, a Node.js app…) acts as a thin client. It does three things:

  1. Collects request data and sends it to POST /v1/decide.
  2. Executes the instruction returned in execution.method.
  3. Falls back to a safe page if anything goes wrong.

The API does not only return a destination URL. In REVERSE_PROXY mode, your integration fetches the target page for you and rewrites its links and forms so the visitor stays on your domain for the whole session.

You are responsible for how you use Cloaker. Make sure your campaigns, landing pages, and traffic routing comply with the terms of every ad platform and search engine you work with, and with the laws that apply to you and your visitors.


How It Works

 Visitor ──► Your server / edge function ──► POST https://api.cloaker.net/v1/decide
                       ▲                                   │
                       │            { status, decision, execution, quota, ... }
                       │                                   │
                       └──────────── execute execution.method ◄──┘

      REVERSE_PROXY · REDIRECT_301/302 · META_REFRESH · JS_REDIRECT
      IFRAME_ZERO_REDIRECT · SERVE_LOCAL
                       │
                       └── API error / timeout / invalid response ──► Safe page

The decision is made per request, server-side, before any content is sent to the visitor. The only logic your integration needs is: send the request context, run what the API tells you, fail safe.


Quick Start

  1. Create a campaign in the Cloaker dashboard and copy its Campaign API Key.

  2. Pick a safe page URL — the page that should be shown whenever the API cannot give a valid answer.

  3. Choose a platform from Platform Integrations and copy the matching example.

  4. Replace the placeholders:

    PlaceholderReplace with
    YOUR_CAMPAIGN_API_KEY (or __CLOAKER_CAMPAIGN_API_KEY__ in templates)Your campaign's API key
    https://your-safe-page.example (or __CLOAKER_SAFE_PAGE_URL__ in templates)Your fallback page URL
  5. Deploy, then send a test request and verify the response (see Testing Your Integration).


Authentication

Cloaker uses two different kinds of keys. They are not interchangeable.

KeyScopeUsed for
Global API KeyYour whole accountEvery endpoint except /v1/decide — listing campaigns, reading subscription usage, dashboards, WordPress admin panel.
Campaign API KeyA single campaignOnly /v1/decide — the real-time traffic decision.
  • The Global API Key is sent as api_key in the request body.
  • The Campaign API Key is sent as campaign_api_key in the request body.

Treat both keys as secrets. Store them in environment variables or your platform's secret manager (Workers secrets, Vercel env vars, AWS Lambda env vars, Fastly secret store, wp-config.php). Never place them in client-side JavaScript or in a page the visitor can download.


API Reference

Base URL: https://api.cloaker.net/v1

All endpoints accept and return JSON. Send Content-Type: application/json.

POST /v1/decide

Returns the routing decision for one visitor request. Authenticated with the Campaign API Key.

Recommended headers

HeaderValueNotes
Content-Typeapplication/jsonRequired.
X-Providercloudflare-workers, vercel-edge, aws-lambda, fastly-compute, php, wordpress, nodejs, pythonIdentifies your integration type. Use the matching value, or a descriptive name for custom integrations.
X-Provider-Version1.0.0Version of your integration code.

Request body

{
  "ip": "212.125.15.214",
  "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
  "campaign_api_key": "YOUR_CAMPAIGN_API_KEY",
  "data": {
    "headers": { "accept-language": "en-US", "cookie": "..." },
    "query": { "utm_source": "meta" },
    "cloudflare": { "country": "TR", "tlsCipher": "..." }
  }
}
FieldTypeRequiredDescription
ipstring✅The real visitor IP (IPv4 or IPv6). Take it from the correct platform header — see the table below.
user_agentstring✅The visitor's User-Agent header.
campaign_api_keystring✅The campaign's API key.
data.headersobject✅All request headers as a key/value object. Send them unmodified; the more complete, the better the decision.
data.queryobject✅Parsed query-string parameters of the current URL (e.g. utm_source, fbclid).
data.cloudflareobject—Cloudflare request.cf object when available (country, ASN, TLS details…). Send {} elsewhere.
data.vercel_geoobject—Vercel geolocation headers (city, country, region, latitude, longitude). Used by the Vercel example.
data.fastly_geoobject—Result of getGeolocationForIpAddress() on Fastly Compute.

Where to read the visitor IP

PlatformHeader / source
Cloudflare Workerscf-connecting-ip
Vercel Edgex-real-ip, then first value of x-forwarded-for
AWS Lambda (Function URL / API Gateway)cf-connecting-ip → first x-forwarded-for → requestContext.http.sourceIp
Fastly ComputeFastly-Client-IP, then first x-forwarded-for
PHP / WordPressHTTP_CF_CONNECTING_IP → HTTP_X_FORWARDED_FOR → REMOTE_ADDR
Express / Flaskcf-connecting-ip → first x-forwarded-for → socket address (enable proxy trust)

Only trust forwarding headers if your server really sits behind that proxy. If your origin is publicly reachable, a visitor could spoof X-Forwarded-For. Restrict origin access to your CDN/load balancer where possible.

Response body

{
  "status": true,
  "latency_ms": 17.46,
  "decision": {
    "action": "SAFE",
    "verdict": "BLOCK",
    "score": 0,
    "reasons": ["WARMUP_TRAFFIC_IN_PROGRESS"],
    "flags": {
      "is_bot": false,
      "is_vpn": false,
      "is_tor": false,
      "is_datacenter": false,
      "is_crawler": false,
      "geo_mismatch": false,
      "warmup_mode": true
    }
  },
  "execution": {
    "target_url": "https://example.com",
    "http_status": 200,
    "method": "REVERSE_PROXY",
    "meta_refresh": {
      "enabled": false,
      "delay_seconds": 0,
      "html": "<html><head><meta http-equiv=\"refresh\" content=\"0;url=https://example.com\"></head><body></body></html>"
    },
    "iframe": {
      "enabled": false,
      "fullscreen": true,
      "title": null,
      "sandbox": null,
      "style": null
    },
    "reverse_proxy": {
      "enable": true,
      "forward_headers": ["user-agent", "accept-language", "cookie"],
      "inject_base_tag": true,
      "cache_control": "no-store, no-cache, must-revalidate"
    }
  },
  "quota": {
    "plan": "Starter",
    "remaining": 49835,
    "limit": 50000,
    "usage_percentage": 0.33,
    "reset_at": "2027-09-23T15:10:47.673Z"
  },
  "campaign": {
    "id": "afe4aaee-5eae-449d-94c7-be181a65defe",
    "name": "Meta Ads (Facebook/Instagram)",
    "mode": "WARMUP_TRAFFIC",
    "sources": ["meta"],
    "warmup_progress": {
      "warmup_mode": true,
      "current_clicks": 6,
      "required_clicks": 10,
      "is_completed": false
    }
  },
  "client": {
    "ip": "212.125.15.214",
    "geo": {
      "country_code": "TR",
      "country_name": "Türkiye",
      "city": "Muğla",
      "timezone": "Europe/Istanbul"
    },
    "network": {
      "asn": "AS12735",
      "as_name": "TurkNet Iletisim Hizmetleri A.S.",
      "connection_type": "broadband"
    },
    "device": {
      "type": "Desktop",
      "os": { "name": "Windows", "version": "10" },
      "browser": { "name": "Chrome", "version": "153.0.0.0" }
    }
  }
}

Act on execution, not on decision. The decision block explains why a visitor was classified a certain way (useful for logging and debugging). Everything your integration needs in order to respond is already resolved in execution. Do not re-implement routing rules in your own code.

Top-level fields

FieldTypeDescription
statusbooleantrue when the decision succeeded. If false (or missing), treat the response as failed and serve the safe page.
latency_msnumberServer-side processing time of the decision.
decisionobjectClassification details (see below).
executionobjectThe instruction your integration must carry out.
quotaobjectCurrent plan usage. Each decision request is metered against your plan's click quota.
campaignobjectCampaign metadata, mode, and warm-up progress.
clientobjectWhat Cloaker detected about the visitor (geo, network, device).

decision

FieldDescription
actionOutcome label for the visitor (for example SAFE).
verdictVerdict label for logging (for example BLOCK).
scoreNumeric risk/quality score.
reasonsArray of machine-readable reason codes, e.g. WARMUP_TRAFFIC_IN_PROGRESS.
flagsBooleans: is_bot, is_vpn, is_tor, is_datacenter, is_crawler, geo_mismatch, warmup_mode.

execution

FieldDescription
methodWhat to do — see Execution Methods.
target_urlThe destination URL used by the method.
http_statusSuggested HTTP status for the response.
meta_refreshConfig used by META_REFRESH (delay_seconds, ready-made html).
iframeConfig used by IFRAME_ZERO_REDIRECT (fullscreen, style, sandbox, title).
reverse_proxyConfig used by REVERSE_PROXY (forward_headers, inject_base_tag, cache_control; may also include backend_name for Fastly).

campaign.warmup_progress

When a campaign is in warm-up mode, the first required_clicks visits are collected before full routing begins. While is_completed is false, expect decision.flags.warmup_mode: true and the reason WARMUP_TRAFFIC_IN_PROGRESS. Your integration does not need special handling — simply execute execution as usual.

cURL example

curl -X POST https://api.cloaker.net/v1/decide \
  -H "Content-Type: application/json" \
  -H "X-Provider: curl-test" \
  -H "X-Provider-Version: 1.0.0" \
  -d '{
    "ip": "203.0.113.42",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0 Safari/537.36",
    "campaign_api_key": "YOUR_CAMPAIGN_API_KEY",
    "data": {
      "headers": { "accept-language": "en-US,en;q=0.9" },
      "query": { "utm_source": "meta" },
      "cloudflare": {}
    }
  }'

POST /v1/campaigns/list

Lists all campaigns of the account. Authenticated with the Global API Key.

curl -X POST https://api.cloaker.net/v1/campaigns/list \
  -H "Content-Type: application/json" \
  -d '{ "api_key": "YOUR_GLOBAL_API_KEY" }'
{
  "status": true,
  "message": "User campaigns fetched successfully",
  "data": [
    {
      "id": "afe4aaee-5eae-449d-94c7-be181a65defe",
      "name": "Meta Ads (Facebook/Instagram)",
      "description": "Campaign for Meta Ads (Facebook/Instagram) traffic.",
      "status": true,
      "mode": "FORCE_MONEY",
      "sources": [
        {
          "name": "Meta Ads (Facebook/Instagram)",
          "icon": "https://www.google.com/s2/favicons?domain=meta.com"
        }
      ],
      "createdAt": "2026-09-24T17:14:31.093Z",
      "updatedAt": "2026-09-29T20:17:57.724Z"
    }
  ]
}
FieldDescription
idCampaign ID (UUID).
name / descriptionHuman-readable label and description.
statustrue if the campaign is active.
modeCampaign mode (for example FORCE_MONEY; WARMUP_TRAFFIC is reported by /v1/decide while a campaign is warming up).
sourcesTraffic sources the campaign is configured for, with display icon.
createdAt / updatedAtISO 8601 timestamps.

POST /v1/subscription/usage

Returns plan status and click usage. Authenticated with the Global API Key.

curl -X POST https://api.cloaker.net/v1/subscription/usage \
  -H "Content-Type: application/json" \
  -d '{ "api_key": "YOUR_GLOBAL_API_KEY" }'
{
  "status": true,
  "message": "User limits fetched successfully",
  "data": {
    "plan": "Starter",
    "status": "ACTIVE",
    "usage": {
      "clicksUsed": 182,
      "clicksRemaining": 49818,
      "percentageUsed": 0.364,
      "resetDate": "2026-09-30 23:59:59"
    }
  }
}
FieldDescription
planCurrent plan name.
statusSubscription status, e.g. ACTIVE.
usage.clicksUsed / clicksRemainingUsed and remaining clicks in the current period.
usage.percentageUsedPercentage of the quota consumed.
usage.resetDateWhen the counter resets.

These Global-API-Key endpoints are intended for dashboards, monitoring, and tooling — not for the per-visitor hot path. If you call them from a website, cache the result (the WordPress plugin caches them for 5 minutes).


Handling errors

Never let an API problem break your site or expose an error page to a visitor. Treat all of the following as "fail safe → serve the safe page":

  • Network error or timeout.
  • Non-2xx HTTP status.
  • Response is not valid JSON.
  • status is not true, or execution is missing.
  • execution.method requires target_url and it is empty.

Use a short timeout (3–5 seconds is a good range). The decision call runs in the visitor's request path, so a slow API directly delays the page.


Execution Methods

Run the branch that matches execution.method. Everything you need is in the execution object.

MethodWhat your integration does
REVERSE_PROXYFetch target_url server-side (mirroring the visitor's path and query), forward the configured headers, rewrite redirects/links/forms to your domain, and stream the result back. The visitor's address bar never leaves your domain.
REDIRECT_301 / REDIRECT_302Respond with an HTTP redirect to target_url using status 301 or 302.
META_REFRESHReturn 200 OK with execution.meta_refresh.html. If it is empty, build <meta http-equiv="refresh" content="DELAY;url=TARGET"> yourself.
JS_REDIRECTReturn 200 OK with a minimal HTML page that runs window.location.replace(target_url).
IFRAME_ZERO_REDIRECTReturn 200 OK with a page containing a single <iframe> pointing at target_url, styled by execution.iframe.
SERVE_LOCALServe target_url directly to the client without HTML rewriting. In WordPress, an empty or same-host target means "render the site normally".
anything elseIf target_url exists, redirect with 302. Otherwise fall back to the safe page.

All the HTML-producing methods also send Client Hints headers.

Method details

Redirects (REDIRECT_301, REDIRECT_302) — the fastest, cheapest method. The visitor sees the destination URL in the address bar. Use 302 for temporary routing (the usual choice) and 301 only when the move is permanent, since browsers cache 301 aggressively.

META_REFRESH — useful when you want an HTML page (rather than a Location header) to perform the navigation. delay_seconds controls the delay; meta_refresh.html is a ready-made document you can serve as-is.

JS_REDIRECT — client-side redirect via window.location.replace, which does not leave the interstitial page in the browser history. When you build this page yourself, serialize the URL with JSON.stringify (JavaScript) or json_encode (PHP/Python) so it is always a safe string literal.

IFRAME_ZERO_REDIRECT — renders the target inside a full-page iframe on your URL:

iframe fieldEffect
fullscreentrue → fixed, edge-to-edge iframe (position:fixed; top/left/right/bottom:0; width/height:100%; z-index:999999).
styleCustom inline CSS used when fullscreen is false. Defaults to width:100%;height:100%;border:none;.
sandboxValue for the iframe sandbox attribute (omit when null).
titleValue for the iframe title attribute (accessibility).

Many websites forbid being embedded via X-Frame-Options or a Content-Security-Policy: frame-ancestors header. If the target does not allow framing, the iframe will be blank. Prefer REVERSE_PROXY for those targets.

SERVE_LOCAL — the target is delivered as-is. Edge/serverless examples fetch target_url and return it; the PHP example redirects to it; the WordPress plugin renders the local site when the target is empty or on the same host.


Reverse Proxy In Depth

REVERSE_PROXY is the most capable (and most involved) method. A correct implementation does five things. The Cloudflare, Vercel, Lambda, Fastly, PHP, WordPress, Node.js, and Python examples below all follow this same pipeline.

1. Sync path and query

The visitor's path and query are appended to the target so deep links work.

  • Visitor requests https://yoursite.com/pricing?ref=a
  • target_url is https://target.example/landing/
  • Proxy fetches https://target.example/landing/pricing?ref=a

If the visitor hits / exactly, the target's own path is used unchanged. Query parameters from both sides are merged (the visitor's values win on conflicts).

2. Forward only the allowed headers

Send the headers listed in execution.reverse_proxy.forward_headers. If the list is missing, use a safe default:

user-agent, accept-language, cookie, accept, accept-encoding

Always set Host to the target's host, and set X-Forwarded-For to the visitor IP so the target sees the real client. Where the platform sets Accept-Encoding itself (WordPress HTTP API, requests, fetch), do not forward it.

Cloudflare Error 1000. If the target sits behind Cloudflare, sending CF-Connecting-IP (or multiple/forged X-Forwarded-For values) from your proxy can make Cloudflare reject the request with Error 1000. When proxying to Cloudflare-hosted targets, send exactly one validated X-Forwarded-For value and do not forward any cf-* headers. The WordPress plugin does this by default.

3. Capture redirects

Fetch with redirects set to manual. When the target answers 301/302/303/307/308, rewrite the Location header so that, if it points to the target host (or a subdomain of it), it points to your domain instead. Otherwise the visitor would be bounced off your domain on the first redirect.

4. Rewrite HTML

For text/html responses:

  • Replace absolute links/forms that point to the target host with your origin: href on <a> and action on <form>.
  • If inject_base_tag is true:
    • Prepend <base href="https://target-host/"> to <head> so relative CSS, JS, and images load from the target.
    • Because <base> would also redirect relative links and form actions to the target, rewrite root-relative href="/x" and action="/x" to absolute URLs on your own origin.
  • Leave #anchors, javascript:, mailto:, and tel: links untouched.

Non-HTML content (images, CSS, JS, fonts, JSON) is streamed through unchanged.

5. Respond

  • Keep the target's status code and (non-hop-by-hop) headers.
  • Add the Client Hints headers.
  • If execution.reverse_proxy.cache_control is set, override Cache-Control with it.
  • Preserve every Set-Cookie header. Strip the cookie Domain= attribute so cookies bind to your domain.
  • Forward request bodies for POST, PUT, etc., so forms and checkouts work.

Loop protection

If your site proxies to a URL that eventually points back to your own site, you create an infinite loop. The WordPress plugin tags outgoing proxy requests with X-Cloaker-Proxy: 1 and skips processing any incoming request that carries it. The Node.js and Python examples do the same (they respond 508 Loop Detected). Use the same pattern in custom integrations.

Platform-specific engines

PlatformHTML rewriting engineNotes
Cloudflare WorkersHTMLRewriter (streaming)Fastest — no full-body buffering.
Fastly ComputeHTMLRewriter (streaming)Guarded with typeof HTMLRewriter !== "undefined".
Vercel EdgeRegex over the full textBuffers the HTML; deletes Content-Length.
AWS LambdacheerioNeeds npm install cheerio. Binary bodies returned as base64.
PHP / WordPressRegex callbacksNo extension required.
Node.js (example)cheerio
Python (example)Regex callbacks

Client Hints

Every example adds these response headers so the browser starts sending high-entropy User-Agent Client Hints on subsequent requests. Those extra signals enrich later decisions.

Accept-CH: Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version, Sec-CH-UA-Arch, Sec-CH-UA-Model, Sec-CH-Device-Memory, Sec-CH-Viewport-Width, Sec-CH-DPR
Permissions-Policy: ch-ua=*, ch-ua-mobile=*, ch-ua-platform=*, ch-ua-platform-version=*, ch-ua-arch=*, ch-ua-model=*, ch-device-memory=*, ch-viewport-width=*, ch-dpr=*

Include them on redirects, generated HTML pages, and proxied responses. Because you forward all request headers in data.headers, hints sent by the browser reach the decision engine automatically.


Platform Integrations

All examples are complete and share the same structure. Choose the one that matches where your traffic is served from.

PlatformX-ProviderHTML rewritingBest for
Cloudflare Workerscloudflare-workersHTMLRewriterLowest latency, easiest setup
Vercel Edgevercel-edgeRegexSites already hosted on Vercel
AWS Lambdaaws-lambdacheerioAWS stacks (Function URL / API Gateway)
Fastly Computefastly-computeHTMLRewriterFastly CDN customers
PHPphpRegexAny shared hosting or VPS
WordPresswordpressRegexWordPress sites, no code
Node.js (Express)nodejscheerioSelf-hosted Node servers
Python (Flask)pythonRegexSelf-hosted Python servers

Cloudflare Workers is the best fit for REVERSE_PROXY: HTMLRewriter rewrites links and forms while streaming, so there is no extra latency and no memory overhead. The Worker receives request.cf (country, ASN, TLS details) which is forwarded to the decision engine as data.cloudflare.

Setup

  1. Create a Worker (dashboard → Workers & Pages, or npm create cloudflare@latest).
  2. Paste the code below into index.js and replace the two placeholders.
  3. Prefer secrets over hard-coded keys: wrangler secret put CAMPAIGN_API_KEY, then read env.CAMPAIGN_API_KEY instead of the literal string.
  4. Attach the Worker to your domain with a Route such as yourdomain.com/* (the zone must be proxied by Cloudflare).
  5. npx wrangler deploy.

Behavior notes

  • Visitor IP: cf-connecting-ip.
  • On API errors the Worker returns fetch(DEFAULT_SAFE_PAGE_URL, request).
  • Proxy requests send CF-Connecting-IP and X-Forwarded-For; if your target is also behind Cloudflare, see the Error 1000 note.
cloudflare-workers.js
const DEFAULT_SAFE_PAGE_URL = "https://your-safe-page.example"; 

const CLIENT_HINTS_HEADERS = {
  "Accept-CH": "Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version, Sec-CH-UA-Arch, Sec-CH-UA-Model, Sec-CH-Device-Memory, Sec-CH-Viewport-Width, Sec-CH-DPR",
  "Permissions-Policy": "ch-ua=*, ch-ua-mobile=*, ch-ua-platform=*, ch-ua-platform-version=*, ch-ua-arch=*, ch-ua-model=*, ch-device-memory=*, ch-viewport-width=*, ch-dpr=*"
};

export default {
  async fetch(request, env, ctx) {
    const currentUrl = new URL(request.url);

    const payload = {
      ip: request.headers.get("cf-connecting-ip"),
      user_agent: request.headers.get("user-agent") || "",
      campaign_api_key: "YOUR_CAMPAIGN_API_KEY",
      data: {
        headers: Object.fromEntries(request.headers.entries()),
        query: Object.fromEntries(currentUrl.searchParams.entries()),
        cloudflare: request.cf || {},
      }
    };

    try {
      const response = await fetch("https://api.cloaker.net/v1/decide", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-Provider": "cloudflare-workers",
          "X-Provider-Version": "1.0.0"
        },
        body: JSON.stringify(payload)
      });

      const decision = await response.json();

      if (decision && decision.status && decision.execution) {
        const { method, target_url, meta_refresh, iframe, reverse_proxy } = decision.execution;

        switch (method) {
          case "REVERSE_PROXY": {
            if (!target_url) break;

            const baseTargetUrl = new URL(target_url);
            const proxyTargetUrl = new URL(baseTargetUrl.toString());

            // 1. Path ve Query Senkronizasyonu (Döngü ve sayfa kaybını önler)
            if (currentUrl.pathname && currentUrl.pathname !== "/") {
              const basePath = baseTargetUrl.pathname.replace(/\/$/, "");
              proxyTargetUrl.pathname = `${basePath}${currentUrl.pathname}`;
            }

            currentUrl.searchParams.forEach((val, key) => {
              proxyTargetUrl.searchParams.set(key, val);
            });

            // 2. İletilecek Başlıklar (Forward Headers)
            const proxyHeaders = new Headers();
            const allowedHeaders = reverse_proxy?.forward_headers || [
              "user-agent",
              "accept-language",
              "cookie",
              "accept",
              "accept-encoding"
            ];

            for (const header of allowedHeaders) {
              const val = request.headers.get(header);
              if (val) proxyHeaders.set(header, val);
            }

            const clientIp = request.headers.get("cf-connecting-ip");
            if (clientIp) {
              proxyHeaders.set("X-Forwarded-For", clientIp);
              proxyHeaders.set("CF-Connecting-IP", clientIp);
            }
            proxyHeaders.set("Host", proxyTargetUrl.host);

            const proxyRequestInit = {
              method: request.method,
              headers: proxyHeaders,
              redirect: "manual" // Hedef sunucu yönlendirme verirse yakalayıp rewrite etmek için
            };

            if (!["GET", "HEAD"].includes(request.method)) {
              proxyRequestInit.body = request.body;
            }

            const originResponse = await fetch(proxyTargetUrl.toString(), proxyRequestInit);
            const responseHeaders = new Headers(originResponse.headers);

            // Client Hints başlıklarını yanıta ekle
            for (const [key, value] of Object.entries(CLIENT_HINTS_HEADERS)) {
              responseHeaders.set(key, value);
            }

            if (reverse_proxy?.cache_control) {
              responseHeaders.set("Cache-Control", reverse_proxy.cache_control);
            }

            // 3. HTTP Yönlendirmelerinin (301/302) Yakalanması ve Kendi Alan Adımıza Çekilmesi
            if ([301, 302, 303, 307, 308].includes(originResponse.status)) {
              const location = responseHeaders.get("Location");
              if (location) {
                try {
                  const resolvedLocation = new URL(location, proxyTargetUrl.origin);
                  // Hedef host ile eşleşen yönlendirmeleri Worker domainine uyarla
                  const cleanTargetHost = baseTargetUrl.hostname.replace(/^www\./, "");
                  if (resolvedLocation.hostname.endsWith(cleanTargetHost)) {
                    const newLocation = new URL(resolvedLocation.pathname + resolvedLocation.search + resolvedLocation.hash, currentUrl.origin);
                    responseHeaders.set("Location", newLocation.toString());
                  }
                } catch (e) {
                  // URL parse hatası durumunda orijinal başlık kalır
                }
              }

              return new Response(null, {
                status: originResponse.status,
                headers: responseHeaders
              });
            }

            const contentType = responseHeaders.get("content-type") || "";

            // 4. HTML Yanıtlarında Link ve Form Hedeflerinin Yeniden Yazılması
            if (contentType.includes("text/html")) {
              let rewriter = new HTMLRewriter();

              const cleanHost = baseTargetUrl.hostname.replace(/^www\./, "");
              const targetHostRegex = new RegExp(`^(https?:)?\/\/(www\.)?${cleanHost.replace(/\./g, "\\.")}`, "i");

              rewriter = rewriter
                .on("a", {
                  element(el) {
                    const href = el.getAttribute("href");
                    if (!href || href.startsWith("#") || href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:")) {
                      return;
                    }

                    // Tam link (https://cloaker.net/...) yakalama
                    if (targetHostRegex.test(href)) {
                      el.setAttribute("href", href.replace(targetHostRegex, currentUrl.origin));
                    } 
                    // <base> tag aktifken göreceli linklerin (/tr/...) hedef siteye kaçmasını önleme
                    else if (reverse_proxy?.inject_base_tag && href.startsWith("/")) {
                      el.setAttribute("href", `${currentUrl.origin}${href}`);
                    }
                  }
                })
                .on("form", {
                  element(el) {
                    const action = el.getAttribute("action");
                    if (!action) return;

                    if (targetHostRegex.test(action)) {
                      el.setAttribute("action", action.replace(targetHostRegex, currentUrl.origin));
                    } else if (reverse_proxy?.inject_base_tag && action.startsWith("/")) {
                      el.setAttribute("action", `${currentUrl.origin}${action}`);
                    }
                  }
                });

              // Relative assetlerin (css, js, resim) hedef siteden yüklenmesi için <base> tag enjeksiyonu
              if (reverse_proxy?.inject_base_tag) {
                rewriter = rewriter.on("head", {
                  element(el) {
                    const baseHref = `${baseTargetUrl.protocol}//${baseTargetUrl.host}/`;
                    el.prepend(`<base href="${baseHref}">`, { html: true });
                  }
                });
              }

              return rewriter.transform(
                new Response(originResponse.body, {
                  status: originResponse.status,
                  statusText: originResponse.statusText,
                  headers: responseHeaders
                })
              );
            }

            return new Response(originResponse.body, {
              status: originResponse.status,
              statusText: originResponse.statusText,
              headers: responseHeaders
            });
          }

          case "REDIRECT_301":
          case "REDIRECT_302": {
            const statusCode = method === "REDIRECT_301" ? 301 : 302;
            const headers = new Headers(CLIENT_HINTS_HEADERS);
            headers.set("Location", target_url);

            return new Response(null, {
              status: statusCode,
              headers: headers
            });
          }

          case "META_REFRESH": {
            const htmlContent = meta_refresh?.html || 
              `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="${meta_refresh?.delay_seconds || 0};url=${target_url}"></head><body></body></html>`;

            return new Response(htmlContent, {
              status: 200,
              headers: { 
                "Content-Type": "text/html; charset=utf-8",
                ...CLIENT_HINTS_HEADERS
              }
            });
          }

          case "JS_REDIRECT": {
            const jsHtml = `<!DOCTYPE html><html><head><script>window.location.replace("${target_url}");</script></head><body></body></html>`;
            return new Response(jsHtml, {
              status: 200,
              headers: { 
                "Content-Type": "text/html; charset=utf-8",
                ...CLIENT_HINTS_HEADERS
              }
            });
          }

          case "IFRAME_ZERO_REDIRECT": {
            const style = iframe?.fullscreen 
              ? "position:fixed;top:0;left:0;bottom:0;right:0;width:100%;height:100%;border:none;margin:0;padding:0;overflow:hidden;z-index:999999;" 
              : (iframe?.style || "width:100%;height:100%;border:none;");

            const sandboxAttr = iframe?.sandbox ? `sandbox="${iframe.sandbox}"` : "";
            const titleAttr = iframe?.title ? `title="${iframe.title}"` : "";

            const iframeHtml = `<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>html,body{margin:0;padding:0;height:100%;overflow:hidden;}</style>
</head>
<body>
  <iframe src="${target_url}" style="${style}" ${sandboxAttr} ${titleAttr}></iframe>
</body>
</html>`;

            return new Response(iframeHtml, {
              status: 200,
              headers: { 
                "Content-Type": "text/html; charset=utf-8",
                ...CLIENT_HINTS_HEADERS
              }
            });
          }

          case "SERVE_LOCAL":
            return fetch(target_url, request);

          default:
            if (target_url) {
              const headers = new Headers(CLIENT_HINTS_HEADERS);
              headers.set("Location", target_url);
              return new Response(null, { status: 302, headers });
            }
        }
      }
    } catch (err) {
      // API hatası veya kesinti durumunda safe-page devreye girer
    }

    return fetch(DEFAULT_SAFE_PAGE_URL, request);
  }
};

WordPress Plugin

If your site runs on WordPress, you do not need to write any code. The Cloaker.net plugin implements the same decision pipeline as the edge examples, using WordPress hooks and the WordPress HTTP API.

Requirements: WordPress 5.5+, PHP 7.4+.

Installation

  1. Go to Plugins → Add New → Upload Plugin and upload cloaker-net.zip.
  2. Activate the plugin.
  3. Open the cloaker.net menu in the admin sidebar.
  4. Enter your Campaign API Key (and optionally the Global API Key for dashboard stats), then switch Enabled on.

The admin UI is available in English, Turkish, and Russian (Language: auto follows your WordPress locale).

Settings

SettingDefaultDescription
EnabledoffMaster switch. Nothing happens until this is on and a Campaign API Key is set.
Campaign API Key—Used for /v1/decide.
Global API Key—Used only by the admin panel to list campaigns and show subscription usage (responses cached for 5 minutes).
Safe page URLemptyFallback page. If empty, your own WordPress site is the safe page. If set, the plugin proxies that URL when a decision fails.
API timeout5 sAllowed range 1–15 seconds.
Skip administratorsonLogged-in users with manage_options are never processed, so you can always preview your site.
Include pathsemptyOne path per line. When set, only matching paths are processed. * is a wildcard (e.g. /landing/*).
Exclude pathsemptyOne path per line. Matching paths are never processed (e.g. /wp-json/*, /checkout/*). Exclusions win over inclusions.
Trust proxy headersonRead the visitor IP from CF-Connecting-IP, True-Client-IP, X-Real-IP, then X-Forwarded-For. Turn off if your site is not behind a proxy/CDN.
Disable page cachingonDefines DONOTCACHEPAGE and sends no-cache headers so caching plugins never store a decision meant for one visitor.
Languageautoauto, en, tr, or ru.
Auto-updateonChecks the update manifest for new plugin versions.
LogonKeeps the last 30 decisions in the admin panel (time, path, IP, result, method, HTTP status, latency, error).

What the plugin handles for you

  • Runs on the template_redirect hook, before WordPress renders anything.
  • Skips non-page requests automatically: robots.txt, feeds, favicon, and sitemaps.
  • Skips requests carrying X-Cloaker-Proxy (loop protection).
  • Method mapping is identical to the other examples. SERVE_LOCAL with an empty or same-host target lets WordPress render normally; a different host is proxied.
  • Fail-safe: on API errors, timeouts, or invalid responses, visitors get the safe page URL (proxied) or, if none is configured, your normal WordPress site.
  • Reverse proxy drops cf-* and other proxy-specific headers and sends a single validated X-Forwarded-For to avoid Cloudflare Error 1000.
  • Strips Domain= from proxied Set-Cookie headers so cookies bind to your domain.

Advanced configuration

Add constants to wp-config.php if you need to override defaults:

// Use a staging/self-hosted API base (default: https://api.cloaker.net/v1)
define( 'CLOAKER_NET_API_BASE', 'https://api.cloaker.net/v1' );

// Use a custom plugin-update manifest (HTTPS only)
define( 'CLOAKER_NET_UPDATE_URL', 'https://cloaker.net/wordpress/update.json' );

Update manifest format (update.json):

{
  "version": "1.2.1",
  "download_url": "https://cloaker.net/wordpress/cloaker-net-1.2.1.zip",
  "homepage": "https://cloaker.net",
  "requires": "5.5",
  "requires_php": "7.4",
  "tested": "6.8",
  "last_updated": "2026-09-30",
  "description": "cloaker.net for WordPress.",
  "changelog": "<h4>1.2.1</h4><ul><li>Bug fixes</li></ul>"
}

To publish a new version: bump Version and CLOAKER_NET_VERSION in cloaker-net.php, publish the zip (its root folder must be cloaker-net/), then update version and download_url in the manifest.

On uninstall the plugin removes its settings (cloaker_net_settings), its log (cloaker_net_log), and its update cache.


Building Your Own Integration

Your language or platform isn't listed? The contract is small. Here is the whole algorithm in pseudocode:

function handle(request):
    ip       = real client IP (from the trusted proxy header)
    payload  = {
        ip, user_agent: request.headers["user-agent"],
        campaign_api_key: SECRET,
        data: { headers: ALL request headers, query: parsed query string, cloudflare: {} }
    }

    try:
        decision = POST https://api.cloaker.net/v1/decide(payload, timeout = 3-5s)
        if decision.status == true and decision.execution exists:
            switch decision.execution.method:
                REVERSE_PROXY        → proxy(target_url) + rewrite redirects/links/forms
                REDIRECT_301/302     → HTTP redirect to target_url
                META_REFRESH         → serve execution.meta_refresh.html
                JS_REDIRECT          → serve page with window.location.replace(target_url)
                IFRAME_ZERO_REDIRECT → serve full-page iframe of target_url
                SERVE_LOCAL          → serve target_url as-is
                default              → if target_url: redirect 302
            return (when the method produced a response)
    catch any error:
        log it

    return safe_page()          # ALWAYS reachable

Checklist

  • Real visitor IP, never your server's IP.
  • Send all headers and the parsed query string.
  • Short timeout on the API call.
  • Every failure path ends at the safe page.
  • Add Accept-CH / Permissions-Policy to generated responses.
  • Never cache decision responses shared between visitors (Cache-Control: no-store or the value from reverse_proxy.cache_control).
  • Redirect capture + HTML link/form rewriting for REVERSE_PROXY.
  • Loop protection header.
  • Secrets in environment variables.

Client-Side SDK (Coming Soon)

A lightweight browser SDK (cloaker-client) is in preparation. It will only collect and process extra client-side information from the visitor (for example, browser signals that are not available in HTTP headers) and send it to the API.

The SDK does not perform redirects, and it is not live yet. Routing decisions remain entirely server-side through POST /v1/decide, as described in this guide.


Testing Your Integration

  1. Call the API directly with the cURL example. You should get "status": true and an execution object. Check quota to confirm the call was counted.
  2. Hit your deployed endpoint from a normal browser and from curl -i https://yourdomain.com/. Confirm status code, Location header, or HTML body match the execution.method you expect.
  3. Test the failure path: temporarily set a wrong Campaign API Key (or block api.cloaker.net) and confirm that the safe page is served, not an error.
  4. Test deep links and forms in REVERSE_PROXY mode: open an inner page directly (/pricing), submit a form, follow an internal redirect — the address bar must stay on your domain.
  5. Check response headers: Accept-CH, Permissions-Policy, and (when configured) your Cache-Control should be present.
  6. Monitor usage with /v1/subscription/usage or the dashboard.

Troubleshooting

SymptomLikely causeFix
Always the safe pageWrong or missing Campaign API Key; API unreachable; timeout too shortVerify the key, call /v1/decide with cURL, check outbound network rules, raise the timeout.
403/401-style errors from the APIA Global API Key was used for /v1/decide (or vice-versa)Use the Campaign key for decide; the Global key for everything else.
Redirect loopTarget points back to your own domain; X-Cloaker-Proxy not honoredAdd loop protection; make sure the target host differs from your own.
Page loads but CSS/JS/images are brokeninject_base_tag is off and assets are root-relativeEnable inject_base_tag in the campaign, or proxy asset paths too.
Links jump to the target's domainHTML rewriting missing or regex doesn't matchVerify the rewriter runs for text/html; check target host (with/without www).
Cloudflare Error 1000 from the targetForwarding cf-* headers or duplicate X-Forwarded-ForSend one validated X-Forwarded-For; skip cf-* headers.
ERR_CONTENT_DECODING_FAILEDBody already decompressed but content-encoding header copiedRemove content-encoding and content-length from the proxied response.
Logins/carts don't persistcookie not in forward_headers, or Set-Cookie lost / bound to target domainForward cookies; keep all Set-Cookie headers; strip Domain=.
Everyone sees the same pageA cache is storing the first decisionDisable caching on the route; use no-store; WordPress: keep Disable page caching on.
Blank iframeTarget sends X-Frame-Options / frame-ancestorsUse REVERSE_PROXY instead of IFRAME_ZERO_REDIRECT.
Wrong visitor IP in the dashboardReading the proxy's IP instead of the client'sUse the correct header from the IP table; enable proxy trust.
Lambda times outDefault 3 s timeoutIncrease to 10–15 s.
Quota errors / fallbacksPlan limit reachedCheck quota.remaining and /v1/subscription/usage; upgrade the plan.

Security & Best Practices

  1. Keep keys server-side. Use environment variables or secret stores; rotate a key if it was ever exposed.
  2. Fail safe, always. The safe page is mandatory in every integration; it protects your site when the API is slow or unavailable.
  3. Use a strict timeout (3–5 s) and never retry in the request path.
  4. Do not cache decisions across visitors.
  5. Trust IP headers only from your own proxy/CDN. Restrict direct origin access if possible.
  6. Verify TLS. Do not disable certificate verification in production proxy code.
  7. Escape what you interpolate. When you build HTML yourself, escape target_url (JSON.stringify for scripts, attribute escaping for src/href).
  8. Be careful with logs. IPs and headers are personal data in many jurisdictions; log only what you need and for as short a time as possible.
  9. Watch your quota. Poll /v1/subscription/usage from a monitoring job (not from page requests) and alert before you hit the limit.

Quick Reference

TaskEndpointKey
Get a routing decisionPOST /v1/decideCampaign API Key
List campaignsPOST /v1/campaigns/listGlobal API Key
Read plan usagePOST /v1/subscription/usageGlobal API Key

Execution methods: REVERSE_PROXY · REDIRECT_301 · REDIRECT_302 · META_REFRESH · JS_REDIRECT · IFRAME_ZERO_REDIRECT · SERVE_LOCAL

Known X-Provider values: cloudflare-workers · vercel-edge · aws-lambda · fastly-compute · php · wordpress · nodejs · python

On this page