Publisher API

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:

URLWhen the panelist is sent here
Complete URLSurvey finished successfully
Screen-out URLDid not qualify (quota full, mismatch, security)
Cancelled URLRequired. Reserved for sessions that end without a result; mypinio does not currently redirect panelists to it. Never credit a reward here
Redirect signing secretMin. 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}

PlaceholderReplaced with
{locale}Language segment derived from the locale in your surveys request (e.g. de-DE → de)

Example:

https://panel.example.com/{locale}/survey-return

With 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:

ParameterAlwaysDescription
appIdYesYour app ID
panelistIdYesYour panelist ID from the API request
entryLinkCodeYesShort code for the wall entry used
tsYesUnix timestamp (seconds) when the link was signed
sigYesHMAC-SHA-256 hex signature
reasonScreen-out onlyoverquota, 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.

ParameterPresentDescription
subId1, subId2, subId3Only when you sent them in the surveys requestYour own tracking IDs from the POST /api/v1/publishers/surveys call that listed this offer
surveyIdComplete, screen-out, and cancelledThe surveyId of the offer the panelist started
cpiComplete onlyThe 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}
PartValue
VersionAlways 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
  • ts is 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) <= 900

Test 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:   28eda644c373e5ab96baa3329bef37215763fa2ba5610d9ed0dad590644c3e70

Screen-out — same query plus reason=overquota:

signing string: 1|screenout|app_demo_123|user_abc123|AB12CD34|overquota|1760000000
expected sig:   d761259519708c006c55a3841c45100fde8caca2009e011ff747308a62433c88

Node.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

  1. Credit once. Store each entryLinkCode you have processed and ignore repeats — panelists reload pages and browsers retry.
  2. Credit the user identified by panelistId; do not rely on a login cookie (it may be missing in WebViews and external browsers).
  3. Show a clear result page with a link back to your offer list.
  4. 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:

ValueTypical meaning
overquotaSurvey quota filled
qualification_mismatchPanelist did not match targeting
securitySession failed security checks

Checklist

  • Complete, screen-out, and cancelled URLs saved
  • Signing secret ≥ 16 characters, stored server-side
  • Return handler verifies sig before side effects and passes the test vector
  • Each entryLinkCode credited 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

On this page