Integration documentation for tracking, postbacks, lead submission, and qualification endpoints.
Base URL:
https://app.datajoker.com
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.
/c/:token
Primary click tracking endpoint. The :token is a unique, pre-generated
click token that encodes offer, publisher, and sub-ID information.
| Parameter | Type | Required | Description |
|---|---|---|---|
token |
string | Required | Pre-generated click token |
| Parameter | Type | Description |
|---|---|---|
sub1–sub5 |
string | Sub-ID tracking parameters passed through to postbacks |
GET https://app.datajoker.com/c/abc123def456?sub1=campaign_a&sub2=landing_v2
302 Redirect to the advertiser's landing page with the click ID appended.
/c/:offer_id/:publisher_slug
Legacy click tracking endpoint. Will be sunset in favor of token-based tracking.
| Parameter | Type | Required | Description |
|---|---|---|---|
offer_id |
string | Required | The offer's short ID or UUID |
publisher_slug |
string | Required | Publisher's unique slug identifier |
GET https://app.datajoker.com/c/offer-short-id/pub-slug?sub1=test
302 Redirect to the advertiser's landing page.
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.).
/postback
| 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 |
GET https://app.datajoker.com/postback?click_id=abc-123&revenue=4.50&status=approved&transaction_id=TXN-001
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"
}'
HTTP 200 OK
{
"status": "ok",
"click_id": "abc-123",
"conversion_id": "conv-456"
}
HMAC-SHA256(secret, click_id + revenue + status)
and pass it as the signature
parameter.
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.
/leads/:campaign_slug
| Header | Required | Description |
|---|---|---|
Content-Type |
Required | application/json |
X-Api-Key |
Required | Affiliate API key provided during onboarding |
| Parameter | Type | Required | Description |
|---|---|---|---|
campaign_slug |
string | Required | Unique slug identifying the lead campaign |
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 |
/specs/leads/YOUR_API_KEY
for campaign-specific field requirements.
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"
}'
{
"status": "accepted",
"lead_id": "lead-789",
"payout": "3.50"
}
{
"status": "rejected",
"reason": "missing required field: email"
}
{
"status": "duplicate",
"reason": "lead already submitted within dedup window"
}
# 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"
}'
Check whether an email or phone qualifies for a specific offer before sending traffic. Uses hashed identifiers for privacy.
/api/qualify
| 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 |
GET https://app.datajoker.com/api/qualify?e=5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8&o=auto-ins
{
"qualified": true,
"offer_id": "auto-ins"
}
{
"qualified": false,
"reason": "duplicate"
}
Buyer pre-ping endpoint. External partners query this to validate users and check buying eligibility before routing traffic.
/in
| Header | Required | Description |
|---|---|---|
X-Api-Key |
Required | Partner API key for authentication |
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 |
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"
}'
{
"match": true,
"redirect_url": "https://advertiser.example.com/lp?click_id=xyz"
}
{
"match": false
}
Pull enrichment token data by token ID. Used by advertisers to retrieve token payloads after a click event.
/api/jt/:token_id
| Parameter | Type | Required | Description |
|---|---|---|---|
token_id |
string | Required | The JokerToken UUID |
GET https://app.datajoker.com/api/jt/550e8400-e29b-41d4-a716-446655440000
{
"token_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"field1": "value1",
"field2": "value2"
},
"expires_at": "2026-04-01T00:00:00Z"
}
| 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 |
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.
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.
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.
<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.
{"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.
status
and then verdict_known
before it reads score. See below.
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.
/api/v1/verify
Look up the score and verdict of one visit by the ID your form field carried.
| 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.
|
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"}'
| 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. |
{
"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": []
}
}
{
"status": "pending",
"id": "Xq1uY3JhbmRvbS1sb29raW5nLXZpc2l0LWlkLXRva2VuX2Zvci10aGUtZG9jcy1wYWdl",
"verdict_known": false,
"retry_after_ms": 1000
}
{
"status": "not_collected",
"id": "Xq1uY3JhbmRvbS1sb29raW5nLXZpc2l0LWlkLXRva2VuX2Zvci10aGUtZG9jcy1wYWdl",
"verdict_known": false
}
| 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.
|
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.
"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).
| 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. |
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.
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.
| 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.
|
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"}'
The fields of the 200 example above, without status, id,
collection_complete
and final. Two differences matter:
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.
| 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. |
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.