DataJoker API Documentation

DataJoker API Reference

Integration documentation for tracking, postbacks, lead submission, and qualification endpoints.

Base URL: https://app.datajoker.com

Endpoints

Click Tracking

Redirects the user to the advertiser's landing page while recording a click. The tracking URL is typically placed in publisher creatives and affiliate links.

GET /c/:token

Primary click tracking endpoint. The :token is a unique, pre-generated click token that encodes offer, publisher, and sub-ID information.

URL Parameters

Parameter Type Required Description
token string Required Pre-generated click token

Optional Query Parameters

Parameter Type Description
sub1–sub5 string Sub-ID tracking parameters passed through to postbacks

Example

GET https://app.datajoker.com/c/abc123def456?sub1=campaign_a&sub2=landing_v2

Response

302 Redirect to the advertiser's landing page with the click ID appended.

GET /c/:offer_id/:publisher_slug

Legacy click tracking endpoint. Will be sunset in favor of token-based tracking.

URL Parameters

Parameter Type Required Description
offer_id string Required The offer's short ID or UUID
publisher_slug string Required Publisher's unique slug identifier

Example

GET https://app.datajoker.com/c/offer-short-id/pub-slug?sub1=test

Response

302 Redirect to the advertiser's landing page.

Postback

Receives conversion notifications from advertisers. Both GET (query-string) and POST (JSON body) are accepted. Advertisers configure their postback URL to point here after a conversion event (sale, lead, signup, etc.).

GET POST /postback

Parameters

Parameter Type Required Description
click_id string Required The click ID associated with the conversion
revenue decimal Optional Revenue amount for the conversion
status string Optional Conversion status (e.g., approved, pending, hold, rejected)
transaction_id string Optional External transaction/order ID for deduplication
signature string Optional HMAC-SHA256 signature for request verification

Example (GET)

GET https://app.datajoker.com/postback?click_id=abc-123&revenue=4.50&status=approved&transaction_id=TXN-001

Example (POST)

curl -X POST https://app.datajoker.com/postback \
  -H "Content-Type: application/json" \
  -d '{
    "click_id": "abc-123",
    "revenue": "4.50",
    "status": "approved",
    "transaction_id": "TXN-001"
  }'

Response

HTTP 200 OK
{
  "status": "ok",
  "click_id": "abc-123",
  "conversion_id": "conv-456"
}
HMAC Signing (optional): If configured, compute HMAC-SHA256(secret, click_id + revenue + status) and pass it as the signature parameter.

Lead Submission

Submit leads into the brokering system. Requires an affiliate API key. The campaign slug in the URL determines which campaign the lead is routed to.

POST /leads/:campaign_slug

Headers

Header Required Description
Content-Type Required application/json
X-Api-Key Required Affiliate API key provided during onboarding

URL Parameters

Parameter Type Required Description
campaign_slug string Required Unique slug identifying the lead campaign

Body (JSON)

Fields vary by campaign. Common fields include:

Field Type Description
email string Lead email address
phone string Lead phone number
first_name string Lead first name
last_name string Lead last name
zip string ZIP / postal code
state string Two-letter state code (US)
ip_address string End-user IP address
action string ping (step 1) or post (step 2) for two-step flows; omit for direct post
ping_id string Required for step 2 of two-step ping/post flow
Exact required fields depend on the campaign configuration. Check your posting spec at /specs/leads/YOUR_API_KEY for campaign-specific field requirements.

Example — Direct Post

curl -X POST https://app.datajoker.com/leads/auto-insurance \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: your-affiliate-api-key" \
  -d '{
    "email": "jane@example.com",
    "phone": "5551234567",
    "first_name": "Jane",
    "last_name": "Doe",
    "zip": "90210",
    "state": "CA",
    "ip_address": "203.0.113.42"
  }'

Response — Success (200)

{
  "status": "accepted",
  "lead_id": "lead-789",
  "payout": "3.50"
}

Response — Validation Error (400)

{
  "status": "rejected",
  "reason": "missing required field: email"
}

Response — Duplicate (409)

{
  "status": "duplicate",
  "reason": "lead already submitted within dedup window"
}

Example — Two-Step Ping/Post

# Step 1: Ping (check buyer availability)
curl -X POST https://app.datajoker.com/leads/auto-insurance \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: your-affiliate-api-key" \
  -d '{
    "action": "ping",
    "email": "jane@example.com",
    "phone": "5551234567",
    "zip": "90210",
    "state": "CA",
    "ip_address": "203.0.113.42"
  }'

# Response: {"status":"accepted","ping_id":"ping-abc-123","quoted_payout":"3.50"}

# Step 2: Post (submit full lead with ping_id)
curl -X POST https://app.datajoker.com/leads/auto-insurance \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: your-affiliate-api-key" \
  -d '{
    "action": "post",
    "ping_id": "ping-abc-123",
    "email": "jane@example.com",
    "phone": "5551234567",
    "first_name": "Jane",
    "last_name": "Doe",
    "zip": "90210",
    "state": "CA",
    "ip_address": "203.0.113.42"
  }'

Qualification

Check whether an email or phone qualifies for a specific offer before sending traffic. Uses hashed identifiers for privacy.

GET /api/qualify

Query Parameters

Parameter Type Required Description
e string One of e/ph required SHA-256 hash of the email address (lowercase, trimmed)
ph string One of e/ph required SHA-256 hash of the phone number (digits only)
o string Required Offer short ID

Example

GET https://app.datajoker.com/api/qualify?e=5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8&o=auto-ins

Response — Qualified

{
  "qualified": true,
  "offer_id": "auto-ins"
}

Response — Not Qualified

{
  "qualified": false,
  "reason": "duplicate"
}

Reactive Buyer

Buyer pre-ping endpoint. External partners query this to validate users and check buying eligibility before routing traffic.

GET POST /in

Headers

Header Required Description
X-Api-Key Required Partner API key for authentication

Query / Body Parameters

Parameters vary by pipe configuration. Common fields:

Parameter Type Description
email string User email address
phone string User phone number
zip string User ZIP code
ip string User IP address

Example

curl -X POST https://app.datajoker.com/in \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: partner-api-key" \
  -d '{
    "email": "user@example.com",
    "phone": "5559876543",
    "zip": "10001"
  }'

Response — Match Found

{
  "match": true,
  "redirect_url": "https://advertiser.example.com/lp?click_id=xyz"
}

Response — No Match

{
  "match": false
}

JokerToken

Pull enrichment token data by token ID. Used by advertisers to retrieve token payloads after a click event.

GET /api/jt/:token_id

URL Parameters

Parameter Type Required Description
token_id string Required The JokerToken UUID

Example

GET https://app.datajoker.com/api/jt/550e8400-e29b-41d4-a716-446655440000

Response

{
  "token_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "field1": "value1",
    "field2": "value2"
  },
  "expires_at": "2026-04-01T00:00:00Z"
}

Common HTTP Status Codes

Status Meaning Description
200 OK Request succeeded
302 Redirect Click tracked; redirecting to advertiser
400 Bad Request Missing or invalid parameters
401 Unauthorized Missing or invalid API key
404 Not Found Resource not found (invalid click token, offer, campaign, etc.)
409 Conflict Duplicate lead or conversion
429 Too Many Requests Rate limit exceeded; retry after the indicated interval
502 Bad Gateway No buyers accepted the lead

JokerShield

Device fingerprinting and bot detection. The JokerShield snippet runs on your page and puts a signed visit ID into a hidden field in your forms. Your server sends that ID to /api/v1/verify and gets back the score and verdict for that visit. There is nothing to store on your side beforehand.

Two keys, and only one of them is a secret. The site key is public: it is the djfp_ key embedded in the snippet on every one of your pages, so anyone can read it. The server key is secret: it starts with djsk_, lives only on your server, and without it nobody can look up a visit. Both are on the Install page of your JokerShield portal. Never call /api/v1/verify from the browser, and never put the server key in a page.

How it works

  1. On the Install page, check your Visit ID form field and paste the snippet into your pages. New accounts start on the standard name, jtid. You can keep it, generate a random name instead (harder for bots to spot, because it differs on every site), or choose your own. Whichever you pick, your server reads the field by that name.
  2. The snippet adds <input type="hidden" name="jtid" value="…"> (with your field name) to every form on the page, including forms added after the page loads, and fills it again on submit. The value is an opaque, encrypted token of varying length: pass it through unchanged and do not parse it. The snippet only ever writes a hidden field: if a form already has a visible field with your field name, that form is left alone and gets nothing, so pick a name none of your forms uses. If a submission arrives without the field, the snippet did not run on that page.
  3. When the form reaches your server, your server POSTs {"id": "<the field's value>"} to /api/v1/verify with your server key in the X-Server-Key header. The site key is not used here.
  4. Your server branches on status and then verdict_known before it reads score. See below.
What the hidden field reaches, and what it cannot. The field is added to every form, including forms that post to a third party (a newsletter tool, a payment page). That third party receives the visit ID as one more field. It cannot read a verdict with it, because that takes your server key, and it cannot use it to report data for the visit either: the ID is encrypted, and it does not reveal the collection session it belongs to. Forms inside an iframe served from another origin (embedded form builders) are out of the snippet's reach and do not get the field at all: a submission from one of them arrives without it, so treat it as unverified.

Authentication

You have two keys, and they do different jobs. The site key is public: it is built into the install snippet and sits in your page source, so it only identifies your site to the collector. The server key (it starts with djsk_) is secret and is the only key /api/v1/verify accepts. Send it in the X-Server-Key header — header only: a server key in a query string is ignored, because query strings end up in access logs. The site key is refused by /api/v1/verify in either header, so a leaked or scraped snippet cannot be used to read verdicts. No server key yet? Press Generate server key on the Install page.

POST /api/v1/verify

Look up the score and verdict of one visit by the ID your form field carried.

Request

Field Type Required Description
id string Required The value of your visit field, exactly as the form submitted it. If the request also carries a legacy token, the token exchange answers and id is ignored.

Example

curl -X POST https://app.datajoker.com/api/v1/verify \
  -H "X-Server-Key: YOUR_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "VALUE_OF_YOUR_VISIT_FIELD"}'

Three answers

HTTP status What it means
200 "ok" The visit is stored. The verdict fields below describe this visit, and are provisional while final is false.
202 "pending" The visit is not stored yet — usually the form beat the collector to our server. Comes with a Retry-After: 1 header and retry_after_ms. There is no score and no classification: there is no verdict yet.
200 "not_collected" Nothing was ever stored for this visit and the collection window (about 35 minutes after the page was served) has closed. Final: it will never be scored.

Response — 200 OK

{
  "status": "ok",
  "id": "Xq1uY3JhbmRvbS1sb29raW5nLXZpc2l0LWlkLXRva2VuX2Zvci10aGUtZG9jcy1wYWdl",
  "fingerprint_hash": "a1b2c3d4e5f6...",
  "score": 12,
  "classification": "human",
  "verdict_known": true,
  "reasons": ["consistent device signals", "no automation markers"],
  "collected_at": "2026-09-23T14:02:11Z",
  "collection_complete": true,
  "final": true,
  "device_profile": {
    "score": 15,
    "classification": "human",
    "verdict_known": true,
    "reasons": ["stable across 4 sessions"]
  },
  "device_history": {
    "total_sessions": 4,
    "bot_session_count": 0,
    "suspicious_session_count": 0,
    "human_session_count": 4,
    "unjudged_session_count": 0,
    "max_score": 18,
    "first_seen_at": "2026-09-01T09:15:00Z",
    "last_seen_at": "2026-09-23T14:02:11Z",
    "device_signal_mismatches": []
  }
}

Response — 202 Accepted

{
  "status": "pending",
  "id": "Xq1uY3JhbmRvbS1sb29raW5nLXZpc2l0LWlkLXRva2VuX2Zvci10aGUtZG9jcy1wYWdl",
  "verdict_known": false,
  "retry_after_ms": 1000
}

Response — 200 OK, never collected

{
  "status": "not_collected",
  "id": "Xq1uY3JhbmRvbS1sb29raW5nLXZpc2l0LWlkLXRva2VuX2Zvci10aGUtZG9jcy1wYWdl",
  "verdict_known": false
}

Fields

Field Type Description
status string "ok", "pending", or "not_collected". Branch on this first.
id string The visit ID you sent, echoed back.
fingerprint_hash string | null The device key for this visit. Stable across visits from the same device. null when the visit never reported enough to derive one; the device objects are then absent.
score integer 0–100 Risk score for this visit. Higher is more suspicious. Meaningless when verdict_known is false — see below.
classification string "human", "suspicious", "bot", "crawler", or "unknown". The first four are judged verdicts. "crawler" is a search-engine crawler we verified by reverse and forward DNS — automation, but automation you probably want to let through. "unknown" means we never reached a verdict.
verdict_known boolean true exactly when classification is one of the four judged verdicts. false on every pending and not_collected answer.
reasons array of string Human-readable explanation, for your operators and support tickets. Not a stable contract — do not key logic on these strings.
collected_at string | null ISO8601 timestamp of the visit.
collection_complete boolean | null false when the collector had not finished when this visit was stored — the visitor left, or submitted your form, before every module reported. Such a visit is still judged from what did arrive: it is "unknown" only when the identity core (navigator, canvas, fonts) never reported. An incomplete visit can therefore be "human", "suspicious" or "bot" on less evidence than a complete one. null when it was not recorded.
final boolean Whether the visit's record is closed. While it is false the verdict is provisional: a later flush from the same page can still update the visit and change its score and classification. Retry a non-final answer the same way as a pending one (below). A record is closed as soon as collection completes, or 90 to 150 seconds after the page stops reporting (90 seconds of quiet, swept once a minute).
retry_after_ms integer On pending only: how long to wait before asking again.
device_profile object The verdict for the device across all its visits, rather than this one visit. Same four fields, same meanings. Present once the device has a history; treat it as optional.
device_history object Session counts and first/last seen for this device. The four per-verdict counters sum to total_sessions exactly. A "crawler" session is counted in unjudged_session_count (there is no crawler counter). device_signal_mismatches is an array of strings naming device signals that disagree across the device's visits. Optional, like device_profile.
Check status, then verdict_known, before you read score. When a visitor leaves before the collector finishes, there is nothing to judge: classification is "unknown" and score is 0 — because nothing ever scored, not because the visit looked human. A rule written as score >= 40 → block reads that unjudged visit as the most human traffic you have. Write it as: verdict_known && score >= 40 → block, and decide separately what you want to do with the unjudged ones. The same goes for a visit we have not stored: a pending answer has no score at all, and a not_collected one never will. Neither is a clean visit, and nor is a submission that arrived without your visit field. Treat all three as not clean — hold them for review rather than accepting them.
Timing: retry a pending or provisional visit, briefly. A fast visitor can submit your form before the visit reaches us, or before the collector has finished. The snippet sends what it has collected the moment the form is submitted, which closes most of that gap, but your server can still ask first — and what it gets back can be a partial visit ("final": false). On 202, or on a 200 with "final": false, wait and ask again. The example below asks four times, waiting 0, 1, 2 and 4 seconds before each attempt: about 7 seconds of waiting across four attempts. If the visit is still pending after that, treat it as not clean. If it is stored but still not final, you have a provisional verdict: use it knowing it came from partial evidence (for example, hold a provisional "human" for review rather than accepting it outright).

Errors

Status error What it means
401 missing_server_key No X-Server-Key header. Sending only the site key gets this too: the site key does not authenticate queries.
401 invalid_server_key The server key is not recognised — including a site key sent where the server key goes. Generate one on the Install page if you have none.
403 account_suspended The account is suspended.
404 visit_not_found The ID is not one of your visits: altered, truncated, or issued to another account. The same answer for all three, by design.
429 — Rate limited. See the table at the foot of this page.
503 service_unavailable We could not look the visit up. Retry; do not treat it as clean.

Putting it together

Your server, when a form arrives. The page needs nothing beyond the snippet: the visit field is already in the submission.

// Node 18+. `form` is the submitted form body; `jtid` is your field name
// (the standard one — use yours if you changed it on the Install page).
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function lookUpVisit(id) {
  let latest = null;
  for (const wait of [0, 1000, 2000, 4000]) { // about 7 s of waiting, 4 attempts
    if (wait) await sleep(wait);

    const res = await fetch("https://app.datajoker.com/api/v1/verify", {
      method: "POST",
      headers: {
        "X-Server-Key": process.env.JOKERSHIELD_SERVER_KEY, // secret — never in a page
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ id }),
    });

    if (res.status === 202) continue;  // pending: not stored yet
    if (!res.ok) return null;          // 4xx/5xx: no verdict
    latest = await res.json();         // "ok" or "not_collected"
    if (latest.status !== "ok" || latest.final) return latest;
    // stored but not final: provisional, ask again
  }
  return latest; // null if still pending; otherwise the last provisional answer
}

const v = form.jtid ? await lookUpVisit(form.jtid) : null;

// Check status and verdict_known before score, every time.
if (!v || v.status !== "ok" || !v.verdict_known) {
  // No field, still pending, never collected, or never judged: NOT clean.
  flagForReview(form);
} else if (v.score >= 40) {
  reject(form);
} else if (!v.final) {
  // Looked clean, but on partial evidence: hold it rather than accept it.
  flagForReview(form);
} else {
  accept(form);
}

Your server key must never reach the browser. All of this belongs on your server.

Legacy / REST: token exchange

Tokens are issued only by POST /api/v1/collect, when your own code posts a collection payload to it. The installed snippet never issues one: it does not call /api/v1/collect, so a page with only the snippet has no token to give you. Use this path only if you collect and post payloads yourself; everyone else should use the visit ID above.

Like the visit-ID lookup, this path is authenticated by your server key only. The site key is refused, so holding a token is not enough to read its verdict.

Request

Field Type Required Description
token string Required The token /api/v1/collect returned to your code. Valid for 300 seconds by default — exchange it as soon as it reaches your server, not on a batch job later.

Example

curl -X POST https://app.datajoker.com/api/v1/verify \
  -H "X-Server-Key: YOUR_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"token": "the-token-from-collect"}'

Response

The fields of the 200 example above, without status, id, collection_complete and final. Two differences matter:

  • The top-level score, classification and reasons reflect the device's latest session, which is not necessarily the visit the token came from.
  • device_profile and device_history are present once the device has a history. On a brand-new device you may get the smaller shape for the first few milliseconds after collection. When fingerprint_hash is null the smaller shape is permanent: there is no device to build a history for. Treat both keys as optional.

Errors

Status error What it means
400 missing_token Neither token nor id in the body.
400 invalid_token The token could not be decrypted. Usually truncated or altered in transit.
403 token_not_owned The token belongs to a different account. Check you are not mixing a staging key with a production token.
410 token_expired Older than your token lifetime (300 seconds by default). Exchange tokens on receipt rather than in a later batch.

Rate Limits

All API endpoints enforce rate limiting. When the limit is exceeded you will receive a 429 Too Many Requests response.

Endpoint Limit Window Scope
Tracking (/c/...) 1,000 req 1 minute Per IP
Postback (/postback) 500 req 1 minute Per IP
Leads (/leads/...) 500 req 1 minute Per API key / IP
Reactive Buyer (/in) 1,000 req 1 minute Per IP
JokerShield (/api/v1/verify, /api/v1/collect) 1,000 req 1 minute Per IP
Authenticated API (/api/...) 100 req 1 minute Per token / user

DataJoker API Documentation — Questions? Contact your account manager.