Redirect URLs
Configure where panelists return after a survey and verify signed query parameters on your server.
Why you configure redirect URLs
Before the surveys API returns offers for an app, you must configure where panelists land when they leave a live survey. Set these in the Publisher Portal under Apps → your app → Settings:
| URL | When the panelist is sent here |
|---|---|
| Complete URL | Survey finished successfully |
| Screen-out URL | Did not qualify (quota full, mismatch, security) |
| Cancelled URL | Required. Reserved for sessions that end without a result; mypinio does not currently redirect panelists to it. Never credit a reward here |
| Redirect signing secret | Min. 16 characters — you use this to verify return links |
If any item is missing, POST /api/v1/publishers/surveys returns HTTP 400:
{
"requestStatus": "false",
"message": "App redirect URLs (complete, screenOut, cancelled) and a redirect signing secret (min. 16 characters) must all be configured before fetching surveys. Set them in the app settings."
}Store the signing secret on your server only — the same value you entered in the portal (no leading or trailing spaces).
Renewing the key. We recommend renewing the signing secret regularly. In the app settings, Generate new key creates a new random key; add it to your server, then save the settings. Exit redirects are signed with the new key from the moment you save, so update your server first (or accept both keys for a short moment). The settings page shows when the key was last renewed.
How results reach you
mypinio reports each result by redirecting the panelist's browser to your URL with signed query parameters. Verify the signature on your server, then credit. If a panelist closes the browser before the redirect, no request reaches you — where available, a server-to-server postback closes that gap and also reports client reversals.
Test your URLs
Apps → your app → Settings → Test redirects sends one signed test request from mypinio's server to each URL (panelist ID redirect_probe_test) and shows the HTTP status your server returned. Only a 2xx or 3xx answer counts as reachable; 4xx and 5xx are shown as errors. A reachable URL does not prove your signature check works — confirm in your own logs that the test request was accepted, and never credit redirect_probe_test. Real returns come from the panelist's browser, not from mypinio's server.
URL templates
Each URL must be a valid http:// or https:// address after placeholders are resolved.
Placeholder {locale}
| Placeholder | Replaced with |
|---|---|
{locale} | Language segment derived from the locale in your surveys request (e.g. de-DE → de) |
Example:
https://panel.example.com/{locale}/survey-returnWith locale: "de-DE" in the API request, mypinio redirects to:
https://panel.example.com/de/survey-return?…Only {locale} is supported.
Use HTTPS in production.
Query parameters on return
When a panelist is redirected to your Complete or Screen-out URL, mypinio appends:
| Parameter | Always | Description |
|---|---|---|
appId | Yes | Your app ID |
panelistId | Yes | Your panelist ID from the API request |
entryLinkCode | Yes | Short code for the wall entry used |
ts | Yes | Unix timestamp (seconds) when the link was signed |
sig | Yes | HMAC-SHA-256 hex signature |
reason | Screen-out only | overquota, qualification_mismatch, or security |
Echoed parameters (optional, not signed)
mypinio also echoes context from the wall session. These parameters are not part of the signing string — they are informational and must not be used to authorize a reward. Existing signature checks keep working unchanged.
| Parameter | Present | Description |
|---|---|---|
subId1, subId2, subId3 | Only when you sent them in the surveys request | Your own tracking IDs from the POST /api/v1/publishers/surveys call that listed this offer |
surveyId | Complete, screen-out, and cancelled | The surveyId of the offer the panelist started |
cpi | Complete only | The cpi (USD, plain decimal string such as 1.25) that was shown for this offer in the surveys list |
Optional parameters may also include mypinioContext — an internal routing flag (ignore unless mypinio support asks you to set it). Treat any other unknown parameters as opaque.
Verify return signatures
Before updating rewards, balances, or session state, verify sig on your server using the same redirect signing secret you configured in the portal.
Signing string
Build a UTF-8 string with pipe separators:
1|{outcome}|{appId}|{panelistId}|{entryLinkCode}|{reason}|{ts}| Part | Value |
|---|---|
| Version | Always 1 |
{outcome} | complete, screenout, or cancelled — must match which URL received the request |
{reason} | Empty string for complete; for screen-out use the reason query value or empty if absent |
{ts} | Value of the ts query parameter (integer seconds) |
Signature
Compute HMAC-SHA-256 of the signing string using your redirect signing secret. Compare the lowercase hex result to the sig query parameter using a constant-time comparison.
Reject the request if:
- Signature does not match
tsis more than 15 minutes older or newer than your server clock- Required query parameters are missing
Use the decoded query values exactly as received. Only appId, panelistId, entryLinkCode, reason and ts are signed — never include echoed parameters (surveyId, cpi, subId1–subId3) in the signing string.
Pseudocode
signingString = "1|" + outcome + "|" + appId + "|" + panelistId + "|" + entryLinkCode + "|" + reason + "|" + ts
expectedSig = HMAC_SHA256_HEX(signingSecret, signingString)
accept if constant_time_equal(expectedSig, sig) and abs(now - ts) <= 900Test vector
Check your code against these fixed values before any real traffic (skip the time check for this fixed ts):
Secret: example-signing-secret-0123456789
Complete — query appId=app_demo_123&panelistId=user_abc123&entryLinkCode=AB12CD34&ts=1760000000:
signing string: 1|complete|app_demo_123|user_abc123|AB12CD34||1760000000
expected sig: 28eda644c373e5ab96baa3329bef37215763fa2ba5610d9ed0dad590644c3e70Screen-out — same query plus reason=overquota:
signing string: 1|screenout|app_demo_123|user_abc123|AB12CD34|overquota|1760000000
expected sig: d761259519708c006c55a3841c45100fde8caca2009e011ff747308a62433c88Node.js
import crypto from 'node:crypto'
export function verifyMypinioReturn(query, outcome, secret, nowSec = Math.floor(Date.now() / 1000)) {
const { appId, panelistId, entryLinkCode, ts, sig } = query
if (!appId || !panelistId || !entryLinkCode || !ts || !sig) return false
if (Math.abs(nowSec - Number(ts)) > 900) return false
const reason = query.reason ?? ''
const signingString = ['1', outcome, appId, panelistId, entryLinkCode, reason, ts].join('|')
const expected = crypto.createHmac('sha256', secret).update(signingString, 'utf8').digest('hex')
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(String(sig).toLowerCase(), 'hex')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}Python
import hashlib, hmac, time
def verify_mypinio_return(q: dict, outcome: str, secret: str) -> bool:
if any(not q.get(k) for k in ("appId", "panelistId", "entryLinkCode", "ts", "sig")):
return False
try:
if abs(time.time() - int(q["ts"])) > 900:
return False
except ValueError:
return False
s = "|".join(["1", outcome, q["appId"], q["panelistId"], q["entryLinkCode"], q.get("reason", ""), q["ts"]])
expected = hmac.new(secret.encode(), s.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, q["sig"].lower())PHP
function verify_mypinio_return(array $q, string $outcome, string $secret): bool {
foreach (['appId', 'panelistId', 'entryLinkCode', 'ts', 'sig'] as $k) {
if (empty($q[$k])) return false;
}
if (abs(time() - (int) $q['ts']) > 900) return false;
$s = implode('|', ['1', $outcome, $q['appId'], $q['panelistId'], $q['entryLinkCode'], $q['reason'] ?? '', $q['ts']]);
return hash_equals(hash_hmac('sha256', $s, $secret), strtolower($q['sig']));
}outcome is complete in your Complete URL handler and screenout in your Screen-out URL handler.
After a valid signature
- Credit once. Store each
entryLinkCodeyou have processed and ignore repeats — panelists reload pages and browsers retry. - Credit the user identified by
panelistId; do not rely on a login cookie (it may be missing in WebViews and external browsers). - Show a clear result page with a link back to your offer list.
- If the signature is older than 15 minutes (page reloaded later), show the result page without crediting again.
Screen-out reasons
When reason is present on a screen-out return:
| Value | Typical meaning |
|---|---|
overquota | Survey quota filled |
qualification_mismatch | Panelist did not match targeting |
security | Session failed security checks |
Checklist
- Complete, screen-out, and cancelled URLs saved
- Signing secret ≥ 16 characters, stored server-side
- Return handler verifies
sigbefore side effects and passes the test vector - Each
entryLinkCodecredited at most once - Test redirects shows 2xx/3xx and the test request was accepted in your logs
-
{locale}tested if you use locale-specific paths - HTTPS on all production return URLs