Partner-Initiated Launch
In this path your backend does not call the Pagamio API. Instead your app redirects the customer's browser or web-view to a Pagamio launch URL carrying a short-lived, signed token. Pagamio verifies the token, opens a widget session for the customer, and redirects into the widget.
Use it when your platform prefers a signed hand-off ("single sign-on") to server-to-server calls. Pagamio enables it per partner during onboarding, where you agree the shared launch secret and the iss value with Pagamio.
The Launch URL
GET {WIDGET_BASE_URL}/w/{partnerCode}/launch?token=<JWT>&return_url=<URL-encoded https URL>
| Query parameter | Required | Description |
|---|---|---|
token | Yes | The signed JWT described below. |
return_url | No | Where the widget sends the customer when they close it and no host bridge can dismiss it. Must be https, contain no user info, be at most 500 characters, and its host must be on your allowed return_url hosts list. Otherwise it is silently dropped. URL-encode the value. |
Outcomes
- Success:
302to{WIDGET_BASE_URL}/w/{partnerCode}/#session=<Pagamio session token>. Pagamio's own opaque session token replaces your launch token from this point on. - Any failure:
302to{WIDGET_BASE_URL}/w/{partnerCode}/with no token. The widget shows "This link is no longer valid". The reason is recorded by Pagamio and never shown to the customer, so contact Pagamio support with the approximate time and yourjtiwhen diagnosing.
The response is marked no-store with Referrer-Policy: no-referrer, because the token is in the query string.
The Token
An HS256 JWT signed with your launch secret.
Header
| Field | Required | Description |
|---|---|---|
alg | Yes | Must be HS256. Any other algorithm (including none) is rejected. |
typ | No | If present, must be JWT. |
kid | Recommended | The secret fingerprint. Lets Pagamio pick the right secret during a rotation. |
Claims
| Claim | Required | Description |
|---|---|---|
iss | Yes | Issuer. Must equal the value agreed at onboarding, exactly. |
sub | Yes | Your reference for the customer. Currently must be exactly 10 digits (^[0-9]{10}$). Pagamio uses it as both the customer identifier and the wallet identifier, so it is what your wallet API receives as customerId and walletId. |
iat | Yes | Issued-at, in seconds since the epoch. Must not be later than now plus the clock skew. |
exp | Yes | Expiry, in seconds since the epoch. Rejected once now is later than exp plus the clock skew. Set it to iat + 60: exp - iat may not exceed 60 seconds. |
jti | Yes | A unique token id (at most 200 characters). Each jti is accepted once per partner. A replayed token is rejected. Use a random UUID. |
name | No | Customer display name. Trimmed and cut to 60 characters. |
The clock skew is 30 seconds. Keep your servers on NTP.
Pagamio rejects a token whose lifetime (exp - iat) is more than 60 seconds, or zero or negative. No clock-skew allowance applies to the lifetime, because both claims come from your clock. Issue a token only when the customer taps the button. Because tokens are single-use, never cache or pre-generate them.
Unlike the server-to-server path, a launch token carries no prefill attributes: only sub and name reach the session, so the customer types names or mobile numbers where a product needs them.
The kid Header
kid is the first 12 hexadecimal characters (lower-case) of the SHA-256 digest of your launch secret, taking the secret as its UTF-8 text.
kid = hex( SHA-256( utf8(secret) ) )[0:12]
The HMAC key is likewise the UTF-8 bytes of the secret text as issued. Do not base64-decode it, and remove any whitespace picked up when copying it into configuration.
If kid is present and matches none of Pagamio's configured secrets, the token is rejected. If it is absent, each configured secret is tried.
Signing Examples
Store the secret in your secret manager, never in source code. The examples read it from LAUNCH_SECRET.
Node.js (no dependencies)
const crypto = require("crypto");
const b64url = (input) => Buffer.from(input).toString("base64url");
function buildLaunchUrl({ baseUrl, partnerCode, customerRef, name, returnUrl }) {
const secret = process.env.LAUNCH_SECRET.trim();
const kid = crypto.createHash("sha256").update(secret, "utf8").digest("hex").slice(0, 12);
const now = Math.floor(Date.now() / 1000);
const header = { alg: "HS256", typ: "JWT", kid };
const payload = {
iss: process.env.LAUNCH_ISSUER, // the value agreed at onboarding
sub: customerRef, // 10-digit customer reference
name,
iat: now,
exp: now + 60,
jti: crypto.randomUUID(),
};
const signingInput = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
const signature = crypto.createHmac("sha256", secret).update(signingInput).digest("base64url");
const token = `${signingInput}.${signature}`;
const url = new URL(`/w/${partnerCode}/launch`, baseUrl);
url.searchParams.set("token", token);
if (returnUrl) url.searchParams.set("return_url", returnUrl);
return url.toString();
}
Java (Nimbus JOSE + JWT)
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Date;
import java.util.HexFormat;
import java.util.UUID;
public final class LaunchTokens {
static String fingerprint(String secret) throws Exception {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(secret.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(digest).substring(0, 12);
}
public static String sign(String secret, String issuer, String customerRef, String name) throws Exception {
Instant now = Instant.now();
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256)
.type(JOSEObjectType.JWT)
.keyID(fingerprint(secret))
.build();
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.issuer(issuer)
.subject(customerRef)
.claim("name", name)
.issueTime(Date.from(now))
.expirationTime(Date.from(now.plusSeconds(60)))
.jwtID(UUID.randomUUID().toString())
.build();
SignedJWT jwt = new SignedJWT(header, claims);
jwt.sign(new MACSigner(secret.getBytes(StandardCharsets.UTF_8)));
return jwt.serialize();
}
}
MACSigner requires a secret of at least 32 bytes for HS256, and you should use one that long in any language.
Python (PyJWT)
import hashlib
import os
import time
import uuid
from urllib.parse import urlencode
import jwt # pip install PyJWT
def build_launch_url(base_url, partner_code, customer_ref, name, return_url=None):
secret = os.environ["LAUNCH_SECRET"].strip()
kid = hashlib.sha256(secret.encode("utf-8")).hexdigest()[:12]
now = int(time.time())
token = jwt.encode(
{
"iss": os.environ["LAUNCH_ISSUER"], # the value agreed at onboarding
"sub": customer_ref, # 10-digit customer reference
"name": name,
"iat": now,
"exp": now + 60,
"jti": str(uuid.uuid4()),
},
secret,
algorithm="HS256",
headers={"kid": kid},
)
query = {"token": token}
if return_url:
query["return_url"] = return_url
return f"{base_url}/w/{partner_code}/launch?{urlencode(query)}"
Verification Order And Common Rejections
Pagamio checks, in order: the partner exists and is enabled with a launch configuration; a token is present; the header is HS256 (and typ, if present, is JWT); kid and the signature match a configured secret; iss; exp and iat within the skew; sub is a 10-digit reference; jti is present and unused.
| Symptom (customer sees "This link is no longer valid") | Usual cause |
|---|---|
Signature or kid mismatch | Wrong secret, secret with stray whitespace, or the secret was rotated. Recompute your kid and ask Pagamio to confirm the fingerprint of the secret it holds; the fingerprint is safe to share. |
iss mismatch | The iss differs from the value agreed at onboarding (case sensitive). |
| Expired, or issued in the future | Token older than 60 seconds plus skew, or a server clock that has drifted. |
sub not a 10-digit reference | Wrong customer identifier format. |
| Token already used | The customer tapped twice, the browser re-requested the URL, or a token was replayed. Issue a fresh token per tap. |
Last updated: September 2026