FastCoins is a PTC (Paid-To-Click) publisher network. Embed our ads on your site, your users earn token rewards for viewing them, and you receive a revenue share automatically.
Create a publisher account, verify you own your domain, and get your API credentials once it's reviewed.
-
1Create a publisher account at FastCoins.com/publisher_dashboard. You'll need a site name and URL.
-
2Add a domain in the Domains tab. Each domain has its own API Key, Secret Key, Bearer Token, token config, and postback URL.
-
3Set your postback URL — the endpoint on your server that receives reward notifications (see Postback section below).
-
4Generate a Bearer Token from the Domains tab. Required to authenticate PTC API calls.
-
5Send a test postback from the Domains tab to verify your endpoint responds correctly before going live.
A server-to-server notification sent to your website when a user completes an ad view, so you can automatically credit rewards without any client-side code.
🔐 Security Requirements
- Authorized server IPs:
62.171.140.250,2a02:c207:2329:8601::1— check this against$_SERVER['REMOTE_ADDR']only. This is a direct server-to-server call, not a browser request, so there's no proxy/CDN of ours in between; a header likeX-Forwarded-Foris client-supplied and trivial to spoof, so don't trust it for this check. userIpis required, not just informational — reject the callback if it's missing or not a valid IP. We always resolve and send the real IP of the viewer who completed the ad (through Cloudflare on our side) specifically so you can rely on it for fraud detection, IP-based limits, and duplicate-user checks — an endpoint that ignores it loses that signal entirely.- Required HTTP response: exactly
ok— lowercase, no spaces, no HTML, HTTP 200 - Signature algorithm (recommended):
signature_sha256=HMAC-SHA256(subId + transId + reward, secret_key)—secret_keyis the HMAC key, not concatenated into the message.userIpis not part of it. - Signature algorithm (legacy, still sent on every callback):
signature=MD5(subId + transId + reward + secret_key)— values concatenated with no separators. Kept working indefinitely for integrations built beforesignature_sha256existed; nothing to change if you already verify this one. - Need more than 8 decimals? Every callback also carries
reward_precise(10 decimals) with its ownsignature_sha256_precise=HMAC-SHA256(subId + transId + reward_precise, secret_key)— fully optional and additive, computed the same way, just over the higher-precision string.reward/signature_sha256/signaturekeep sending exactly as before regardless of whether you use it. - Duplicate prevention: always check
transIdin your DB before crediting
📋 Postback Parameters (GET)
Your postback URL receives these parameters via HTTP GET.
| Parameter | Type | Required | Description |
|---|---|---|---|
| subId | string | REQUIRED | Unique user ID in your system — the value you passed in the widget/API call |
| transId | string | REQUIRED | Unique transaction ID — always check this to prevent duplicate credits |
| reward | float | REQUIRED | Reward in your token currency (already converted using your configured token_rate), formatted to 8 decimals |
| reward_precise | float | Optional | Same reward as reward, formatted to 10 decimals instead of 8 — use this when 8 decimals isn't enough precision for you |
| reward_name | string | REQUIRED | Token name as configured for your domain (e.g. POINTS, COINS) |
| reward_value | float | REQUIRED | Same as reward — included for bitcotasks compatibility |
| offer_name | string | Optional | Title of the completed ad — useful for logging |
| offer_type | string | Optional | Always ptc for FastCoins ad views |
| payout | float | Optional | Same as reward — included for third-party compatibility |
| userIp | string | REQUIRED | Real IP address of the user who completed the offer, resolved through Cloudflare on our side before we send this callback — reject the callback if this is missing or fails FILTER_VALIDATE_IP. Use it for fraud detection, IP-based limits, duplicate-user detection, conversion auditing, and analytics. |
| status | integer | REQUIRED | 1 = Credit reward | 2 = Chargeback (deduct reward) |
| signature_sha256 | string | REQUIRED | HMAC-SHA256 of subId + transId + reward, keyed with secret_key — recommended for new integrations |
| signature_sha256_precise | string | Optional | HMAC-SHA256 of subId + transId + reward_precise, keyed with secret_key — verify this instead of signature_sha256 only if you're also reading reward_precise |
| signature | string | REQUIRED | Legacy: MD5 of subId + transId + reward + secret_key concatenated (no separators). Still sent on every callback, unchanged — kept for integrations built before signature_sha256 existed. |
| test | integer | Optional | 1 = test postback from dashboard — do not credit real rewards |
🧮 Example: Building & Verifying a Signed URL
A full worked example so you can sanity-check your own signature calculation against ours before going live. This uses a made-up secret key purely for illustration.
Secret Key (example only, never a real one): demo_secret_do_not_use_9f8e7d6c Parameters for this conversion: subId = user_12345 transId = txn_987654321 reward = 0.50 status = 1 userIp = 127.0.0.1 signature_sha256 = HMAC-SHA256(subId + transId + reward, secret_key) = HMAC-SHA256("user_12345" . "txn_987654321" . "0.50", demo_secret_do_not_use_9f8e7d6c) = HMAC-SHA256("user_12345txn_9876543210.50", demo_secret_do_not_use_9f8e7d6c) = df3eafaa3e8cf4fe8fb40c7bed633f34bc7b964bca58d2d691314771a042d59c signature (legacy) = MD5(subId + transId + reward + secret_key) = MD5("user_12345" . "txn_987654321" . "0.50" . "demo_secret_do_not_use_9f8e7d6c") = MD5("user_12345txn_9876543210.50demo_secret_do_not_use_9f8e7d6c") = d1eb68e41cbd5770de9384c5be8ebb39 Resulting callback URL: https://yoursite.com/postback.php?subId=user_12345&transId=txn_987654321&reward=0.50&status=1&userIp=127.0.0.1&signature=d1eb68e41cbd5770de9384c5be8ebb39&signature_sha256=df3eafaa3e8cf4fe8fb40c7bed633f34bc7b964bca58d2d691314771a042d59c
reward_name, reward_value, offer_name, offer_type, payout and test for brevity — your real callback includes all of them, per the table above.📐 Optional: 10-Decimal Precision (reward_precise)
Same secret, same callback — just a reward with more fractional precision than reward's 8 decimals can hold. Fully additive: skip this if 8 decimals already covers your use case.
Same secret key and subId/transId as above, but the underlying reward this time carries more precision: reward = 0.12345679 (8 decimals, rounded) reward_precise = 0.1234567890 (10 decimals) signature_sha256_precise = HMAC-SHA256(subId + transId + reward_precise, secret_key) = HMAC-SHA256("user_12345" . "txn_987654321" . "0.1234567890", demo_secret_do_not_use_9f8e7d6c) = 219d2101a528b8585161bd26c3bb80c5de8a6741d60d2f1778eb805a05f7a707
💻 PHP Implementation — postback.php
Save this on your server, set your Secret Key from the Domains tab, then paste the full URL into the Postback URL field of your domain.
// ── 1. YOUR SECRET KEY (from Domains tab → Secret Key) ───── $secret = ""; // ← paste your secret key here // ── 2. IP VALIDATION (callback source) ────────────────────── // REMOTE_ADDR only — this call comes straight from our server to // yours, no browser and no proxy of ours in between, so it's the // one value here that can't be spoofed by whoever is calling you. // X-Forwarded-For (or any other header) is attacker-controlled and // must never be trusted for this check. $allowed_ips = ['62.171.140.250', '2a02:c207:2329:8601::1']; $ip = $_SERVER['REMOTE_ADDR'] ?? ''; if (!in_array($ip, $allowed_ips, true)) { echo "ERROR: Invalid source IP"; exit; } // ── 3. COLLECT PARAMETERS ─────────────────────────────────── $userId = $_GET['subId'] ?? null; $transId = $_GET['transId'] ?? null; $status = $_GET['status'] ?? null; $isTest = isset($_GET['test']) && $_GET['test'] == 1; $userIp = $_GET['userIp'] ?? null; // real IP of the ad viewer — required, see step 4 // ── 3b. DYNAMIC PRECISION ──────────────────────────────────── // Prefer reward_precise (10 decimals) when the callback carries it; // fall back to the standard reward (8 decimals) otherwise. Whichever // pair is picked, it's verified against its own matching signature in // step 6 — if that signature is valid, the postback is accepted. $hasPrecise = isset($_GET['reward_precise'], $_GET['signature_sha256_precise']); $reward = $hasPrecise ? $_GET['reward_precise'] : ($_GET['reward'] ?? null); $signature = $hasPrecise ? $_GET['signature_sha256_precise'] : ($_GET['signature_sha256'] ?? null); // ── 4. VALIDATE USER IP (required) ────────────────────────── // userIp is mandatory — this is the real IP of the person who // completed the offer, already resolved through Cloudflare on our // end. Reject the callback outright rather than silently accepting // one without it, since your fraud detection / IP-limit / duplicate- // user checks depend on this value actually being present and valid. if (!$userIp || !filter_var($userIp, FILTER_VALIDATE_IP)) { echo "ERROR: Invalid user IP"; exit; } // ── 5. VALIDATE REQUIRED PARAMS ───────────────────────────── if (!$userId || !$transId || $reward === null || !$signature) { echo "ERROR: Missing parameters"; exit; } // ── 6. VERIFY SIGNATURE ───────────────────────────────────── // Only subId + transId + reward go into this hash — userIp is NOT // part of it. $secret is the HMAC key, not concatenated into the // message. Don't add userIp here unless a different provider's docs // explicitly say their signature scheme includes it. $expected = hash_hmac('sha256', $userId . $transId . $reward, $secret); if (!hash_equals($expected, $signature)) { echo "ERROR: Signature mismatch"; exit; } // ── 7. HANDLE CHARGEBACK (status = 2) ─────────────────────── if ((int)$status === 2) { $reward = -abs((float)$reward); // negative = deduct } // ── 8. TEST CALLBACK ───────────────────────────────────────── // Authenticated and validated like any other callback, but must not // touch a real balance. if ($isTest) { echo "ok"; exit; } // ── 9. PREVENT DUPLICATES, CREDIT REWARD & STORE USER IP ────── // Replace with your actual DB logic. if (!isTransactionProcessed($transId)) { creditUserReward($userId, $reward, $transId); // If your credit function only takes these 3 args, store $userIp // with the conversion separately, e.g.: // saveConversionIp($transId, $userId, $userIp); } // ── 10. REQUIRED RESPONSE ─────────────────────────────────── echo "ok"; // MUST return exactly this, nothing else
🕓 Legacy Verification — MD5
signature with MD5? It's still sent on every callback, completely unchanged — this is here purely for reference. No action needed. New integrations should use signature_sha256 above instead.$signature = $_GET['signature'] ?? null; $expected = md5($userId . $transId . $reward . $secret); if (!hash_equals($expected, $signature)) { echo "ERROR: Signature mismatch"; exit; } // ...same IP check, userIp check, and crediting logic as the main example above.
🔀 Callback Flow
- 1Callback received
- 2Check
REMOTE_ADDRagainst the IP whitelist - 3Read parameters, including
userIp - 4Validate
userIp— reject if missing or not a valid IP - 5Validate the other required parameters
- 6Verify the signature
- 7Check
status— apply as chargeback if2 - 8If
test=1, returnokwithout crediting anything - 9Check
transIdagainst your DB to prevent duplicate credits - 10Credit or reverse the reward, and store
userIpwith the conversion - 11Return
ok
Possible responses:
ok— callback processed (or validated as a test callback)ERROR: Invalid source IPERROR: Invalid user IPERROR: Missing parametersERROR: Signature mismatch
ok — no spaces, no newlines, no HTML.transId.test=1 — skip crediting for those.Query available PTC ads via a REST API and render them with your own custom UI. No widget embedding required.
🌐 Endpoint
| Segment | Description |
|---|---|
| API_KEY | Your domain's API Key — found in the Domains tab of your Publisher Dashboard |
| USER_ID | Unique identifier of the user in your system. Returned as subId in the postback |
| USER_IP | Real IP address of the user — used for ad targeting and fraud prevention. Pass the actual client IP |
| DEVICE | Exactly desktop or mobile — there is no third value. Tablets (iPad, Android tablets) count as mobile. The visitor's real device — required once any advertiser is running a device-targeted campaign: since you're calling this API from your own server, your server's User-Agent isn't the visitor's, so we can't detect it for you — you must detect it and pass it through. Sending anything other than desktop/mobile (including a literal "tablet") is treated the same as omitting it — map it to mobile yourself. A request missing or misspelling this returns HTTP 400 in that case. |
[DEVICE] yet, add it now. It's silently ignored today, but starts returning HTTP 400 as soon as device targeting goes live — don't wait for that to break your feed.🔑 Authentication — Bearer Token
Authorization header. Generate one per domain from the Domains tab (🔄 button next to "Bearer Token").💻 PHP Example
// ── YOUR CREDENTIALS (from Domains tab) ───────────────────── $apiKey = 'YOUR_API_KEY'; $bearerToken = 'YOUR_BEARER_TOKEN'; // ── USER DATA ──────────────────────────────────────────────── $userId = $_SESSION['user_id']; // your user's unique ID // REMOTE_ADDR by default — it's the one value here that can't be // spoofed by the visitor. Only read a proxy header instead (e.g. // CF-Connecting-IP) if your own server sits behind a reverse proxy // you control and trust — never a client-supplied header like // X-Forwarded-For, which the visitor's own browser can set to // anything. $userIp = $_SERVER['REMOTE_ADDR'] ?? ''; // Required once any advertiser runs a device-targeted campaign — this // script call happens during the visitor's own page load, so THIS // server's incoming User-Agent is theirs. Don't skip this: a call from a // background job/cron with no real visitor attached has no correct value // to send here. $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; $device = preg_match('/Mobi|Android|iPhone|iPod|iPad|BlackBerry|IEMobile|Opera Mini/i', $userAgent) ? 'mobile' : 'desktop'; // ── BUILD URL ──────────────────────────────────────────────── $url = 'https://FastCoins.com/api/' . $apiKey . '/' . urlencode($userId) . '/' . urlencode($userIp) . '/' . $device; // ── CURL REQUEST ───────────────────────────────────────────── $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $bearerToken, 'Accept: application/json', ], ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $data = json_decode($response, true); // ── HANDLE RESPONSE ────────────────────────────────────────── if ($httpCode === 200 && $data['status'] === '200') { foreach ($data['data'] as $ad) { // $ad['url'] already contains the user subId encoded echo '<a href="' . htmlspecialchars($ad['url']) . '" target="_blank">'; echo htmlspecialchars($ad['title']); echo ' — ' . $ad['reward'] . ' ' . $ad['currency_name']; echo '</a><br>'; } } else { echo 'No ads available.'; }
📦 Response Format
Successful responses return HTTP 200 with this JSON structure:
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier of the ad |
| image | string | Full URL of the ad banner image — may be empty |
| title | string | Display title — show this to your users |
| description | string | Short description of the ad |
| duration | string | Required view time in seconds: 2, 30, or 60 |
| reward | float | Reward amount in your domain token currency (already converted) |
| currency_name | string | Token name as configured for your domain (e.g. COINS, USDT) |
| url | string | Redirect the user here to watch the ad. Postback fires automatically on completion. Already encodes the user's subId. |
| boosted_campaign | boolean | Whether this is a promoted ad — consider highlighting these in your UI |
| ad_type | string | Iframe = plays inside a frame | Redirect = user goes to advertiser's page |
⚠️ Error Codes
Error responses share the same JSON structure with a non-200 status field.
| HTTP | status | Cause & Resolution |
|---|---|---|
| 401 | 401 | Invalid API key — check the API_KEY segment in the URL, or Bearer Token is missing when domain requires one |
| 403 | 403 | Bearer token mismatch — regenerate and update it in the Domains tab |
| 400 | 400 | Invalid USER_ID format — must be alphanumeric (a-z, 0-9, _, -, .), max 255 chars |
| 400 | 400 | Missing or invalid DEVICE segment — only returned once device targeting is live. Must be exactly desktop or mobile |
| 500 | 500 | Internal server error — the message field contains debug details. Contact support if this persists. |
End-to-end flow from API call to reward credit on your site.
📡 API + Postback Flow
GET /api/…urlGET postback.phpok🧩 Widget Embed (alternative)
If you prefer not to use the API, embed the widget as an iframe — it handles everything automatically, style-isolated from the rest of your page.
<iframe src="https://fastcoins.click/widget-embed.php?api_key=YOUR_API_KEY&sub_id={USER_ID}" style="width:100%;height:640px;border:none;" loading="lazy"></iframe>
YOUR_API_KEY with your domain's API Key and {USER_ID} with the current user's ID (server-side rendered). The widget displays available ads and tasks inline and fires the postback automatically on completion. Adjust the 640px in style to make the iframe taller or shorter for your layout.sub_id (= {USER_ID}) per real user — no suffix splitting. Each of your users must be identified by a single, stable id. You choose the format — an opaque id, a username, an email (e.g. ABD01230A23, [email protected]) — but one real user maps to exactly one id, forever. Not permitted: taking one base id and appending a numeric suffix to manufacture many "users" out of one real person. The detection is separator-agnostic — it strips a trailing
-, _ or . followed by digits and treats the rest as the user's identity. Any of these pairs is rejected: 123-1&123-2ABC_1&ABC_2A23-23&A23-24order.1&order.2
ABD01230A23, 1001, [email protected] are all fine). When the pattern is detected, the API still answers
ok but returns a message explaining the id is not allowed and no reward is credited; the attempt is logged as fraud. Create a task (e.g. "register at my site"), fund N completions, and confirm each one with a signed server-to-server postback. The user is credited automatically the moment your postback arrives — no manual approval, no platform hold.
📡 How it works
/create-taskcka_cid1) The postback URL
GET https://fastcoins.click/webhooks/task-postback.php?cka_cid={CLICK_ID}&sign={SIGN}cka_cid— the click id we appended to your destination URL when the user started (your-url?cka_cid=…). Echo it back unchanged.sign—hash_hmac('sha256', $cka_cid, $task_secret)(lowercase hex). The per-task secret is shown in Create Task → My Tasks.
2) Signature
// PHP
$sign = hash_hmac('sha256', $cka_cid, $task_secret);
// Node.js
const sign = crypto.createHmac('sha256', taskSecret).update(ckaCid).digest('hex');
# Python
import hmac, hashlib
sign = hmac.new(task_secret.encode(), cka_cid.encode(), hashlib.sha256).hexdigest()403 even with a valid signature. This + the HMAC is the whole authenticity model — keep your secret safe.3) Optional analytics pixel
Place this on your conversion page for your own analytics. It never credits — the postback above is the only thing that does.
<img src="https://fastcoins.click/task-pixel.php?cid={CLICK_ID}" width="1" height="1" alt="">Recommendations for a reliable, secure, and high-revenue integration.
⚡ Performance
- Cache API responses for at least 60 seconds per user. Don't call the API on every page load — ads change at most every few minutes.
- Pass the real user IP. Use
REMOTE_ADDRby default — it's the one value that can't be spoofed by the visitor. Only read a proxy header instead (e.g.CF-Connecting-IP) if your own server sits behind a reverse proxy you control and trust; never a client-supplied header likeX-Forwarded-For. Incorrect IPs reduce ad targeting quality. - Set a 10-second timeout on your cURL calls to prevent slow page loads if our API is temporarily slow.
🔒 Security
- Never expose your Secret Key in client-side code or public repositories. It is used only server-side for signature validation.
- Always check
transIdagainst your database before crediting — this prevents duplicate rewards from accidental postback resends. - Validate source IP before processing any postback. Only accept requests from
62.171.140.250or2a02:c207:2329:8601::1. - Use HTTPS for your postback endpoint. HTTP endpoints may be blocked.
- Send one unique
sub_idper real user. Don't append numeric suffixes to one base id (123-1/123-2,ABC_1/ABC_2,order.1/order.2) — any separator (-_.) is detected as fraud and rejected with no reward credited. See the rule above.
💰 Revenue
- Boosted ads (
boosted_campaign: true) pay more — highlight them in your UI to increase click-through rates. - Longer ads (60s) pay significantly more than short ads (2s). Prioritize them when user engagement is high.
- Balance your user share — a higher payout attracts more active users; a lower payout increases your own margin.
- Withdrawals are open from the 1st to 3rd of each month — mark it in your calendar.
🐛 Debugging
- Use the Test Postback button in the Domains tab to verify your endpoint returns exactly
okwith HTTP 200. - Check the Postback Logs tab for full HTTP responses on every postback attempt — it shows status codes and server replies.
- Failed postbacks can be resent individually or in bulk from the Logs tab. No data is ever permanently lost.
- If the signature always fails: for
signature_sha256, verifysecret_keyis passed as the HMAC key (not concatenated into the message) and you're hashingsubId + transId + rewardwith no separators; for the legacysignature, verify you're concatenatingsubId + transId + reward + secretwith no separators. Either way, make surerewardmatches the exact string from the GET parameter (not rounded). - If you're verifying
signature_sha256_precise: it's hashed overreward_precise, notreward— mixing the two fields with the wrong signature is the most common mistake here.
Ready to start earning?
Create your publisher account in under 2 minutes, then verify your domain to get started.
Open Publisher Dashboard →