Hosted Pay Page Signed Requests
Overview
The Signed Hosted Pay Page feature locks the values you send to a Segpay pay page to prevent tampering between your server and Segpay. You sign a JSON Web Token (JWT) containing the field value you want protected, and include the JWT in your pay page request as a parameter named jwt.
Use this when you need to guarantee:
-
The amount charged matches the amount specified on your site.
-
Customer-supplied fields (email, username, etc.) reach Segpay unchanged.
-
Requests to your pay page can’t be modified by third parties.
Compatibility:
-
Signed requests are an additional layer on top of your existing integration.
-
Unsigned fields continue to work as before (you only sign the fields you want to protect).
Prerequisites
Configured by Segpay Merchant Services
Before starting your integration, request the following from Segpay Merchant Services:
-
Enable Require signed amount on your pay page. This activates signing enforcement for the amount field.
-
Enable Require Signing on additional fields you want protected from tampering (optional).
-
Provide a signing key. Base64-encoded 256-bit value.
Example:gT+v31hCKBobC0QXNozy4/UNpGGy0+L8DKgbt4qOGmM=
Developer setup
-
A JWT library for your server-side language.
-
Node.js:
jsonwebtoken -
Python:
pyjwt -
PHP:
firebase/php-jwt
Working in a different language? Any JWT library supporting HS256 will work. The pattern is the same: build the payload, sign with the key, include the resulting token as the jwt parameter on your pay page request.
-
-
Secure storage for the signing key (environment variable, secrets manager, or equivalent). Never commit the key to source control or expose it in client-side code.
Signing protection is optional per field.
A field is protected only if Require Signing is enabled for it on the pay page. Including a field inside your JWT fields object without activating Require Signing on that field has no effect. The pay page accepts any value via URL or POST, signed or not. Confirm with Merchant Services which fields have signing enabled before relying on their protection.
Quick Start
This section walks you through a minimal, end-to-end integration. It assumes Segpay Merchant Services has enabled Require signed amount on your pay page and provided a signing key. The key shown here is for illustration only. Use the key issued to you by Merchant Services.
Step 1. Build the signed token
Build a JWT containing the standard claims and the fields you want to protect, such as “amount.” In this example, the pay page reference is acme-checkout-1,and the amount you want to lock is 29.99:
{
"iat": 1714000000,
"exp": 1714001800,
"jti": "1f7f8a02-9b3a-4f7c-bf85-c5d4e7e2a0a1",
"pageref": "acme-checkout-1",
"fields": {
"amount": "29.99"
}
}
Note: The iat and expvalues are Unix timestamps. Thejtiis a unique identifier for this token; use a UUID. See the JWT Specification section for the full claims reference.
Step 2. Sign it
Sign the token with HS256 using the signing key Segpay provided you. The signing key is a base64-encoded string. The signed token has three sections separated by periods: header, payload, and signature.
Important: Pass the signing key to your HMAC routine as-is. Do not base64-decode it first. The gateway uses the literal 44-character string as the HMAC secret on its verification side. If you decode the key, your signature will not match.
Signed Token Example
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MTQwMDAwMDAsImV4cCI6MTcxNDAwMTgwMCwianRpIjoiMWY3ZjhhMDItOWIzYS00ZjdjLWJmODUtYzVkNGU3ZTJhMGExIiwicGFnZXJlZiI6ImFjbWUtY2hlY2tvdXQtMSIsImZpZWxkcyI6eyJhbW91bnQiOiIyOS45OSJ9fQ.<signature>
Step 3. Send the cardholder to the pay page
Send the signed token to Segpay. You have two options. Choose the one that best fits your checkout flow.
Option A - GET (URL parameter) with this code block:
Option B - POST (form body) with this code block:
<form action="https://pay.segpay.com/acme-checkout-1" method="POST">
<input type="hidden" name="jwt" value="<token>">
</form>
Use POST when the signed token is large (many signed fields or long values). Use GET when refresh or back-button behavior is important in your checkout flow (POST-rendered pages lose the token on refresh).
That’s the full integration. The rest of this guide explains each piece in detail.
Signing Key
The signing key issued to you by Segpay Merchant Services is a base64-encoded string ending in =. Your HMAC routine uses it as the secret when signing each token.
Using the signing key
Pass the signing key to your HMAC routine exactly as Segpay provided it. Do not decode it, trim it, or transform it before signing.
If you modify the signing key before signing, your signature will not match the one Segpay generates during verification. Every signed request from your integration will fail.
Storing the signing key
Store the signing key securely on your server using an environment variable, a secrets manager, or equivalent. Never expose the key in client-side code or commit it to source control.
JWT Specification
Algorithm
Sign tokens using HS256 (HMAC with SHA-256). Other algorithms are rejected.
Required claims
| Claim | Type | Description |
|---|---|---|
iat
|
integer | Issued at timestamp (Unix seconds). |
exp
|
integer | Expiration timestamp (Unix seconds). Maximum 30 minutes from iat. |
jti
|
string | Unique token ID. Use a UUID. Each token must have its own jti. The gateway rejects any token with a jti it has already seen. |
pageref
|
string | The pay page reference provided by Segpay (e.g., 37477-09876). Must match the pay page you’re sending the request to. |
fields
|
object | Key-value map of the field values being signed (e.g., {“amount”: 14.00}). |
Example decoded payload
{
"iat": 1779472838,
"exp": 1779474638,
"jti": "d3b07384-d113-4c4e-a5ac-30d317d7b1aa",
"pageref": "37477-09876",
"fields": {"amount": 14.00}
}
Token uniqueness
Each signed token must contain a unique jti value. The gateway tracks tokens it has previously verified and rejects any token whose jti matches one it has already used, even if the token has not expired.
Generate a fresh token (with a new jti) for each customer checkout. Do not cache tokens to reuse across multiple sessions.
Clock skew tolerance
The gateway accepts up to 120 seconds of clock drift on the iat and exp timestamps. An iat up to 120 seconds in the future is acceptable, and an exp up to 120 seconds in the past is acceptable.
Standard NTP-synchronized servers won't approach this limit. The tolerance exists to cover normal clock drift between servers, not to extend the effective lifetime of tokens. Do not rely on the 120-second window as latitude for reusing expired tokens.
Field rules
Check with Merchant Services to confirm which fields have signing enabled before relying on them being protected. Signing a field that isn’t configured for Require Signing has no enforcement effect. The pay page treats the request the same as if you hadn’t signed it.
-
The
fieldsobject accepts a maximum of 32 entries. Tokens with more than 32 entries are rejected as malformed. -
Fields configured as Require Signing on the pay page must also appear in the
fieldsobject. Sending it as a URL parameter alone will fail and the customer will see an error page. -
Fields not configured as Require Signing can still be sent as plain URL or POST parameters; they don’t need to go into the JWT.
-
Signed fields take precedence.
Example: If you signamount: 14.00and also include?amount=1.00in the URL, the pay page uses the signed value (14.00). -
Fields are automatically hidden on the pay page when configured to require signing. The customer cannot edit them.
Sending the JWT
The signed JWT goes in the pay page request as a parameter named jwt.
As a GET parameter/request
https://pay.segpay.com/37477-09876?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3ODAwNzk2MDIsImV4cCI6MTc4MDA4MTQwMiwianRpIjoiNTdmZWU4NGYtNjBkMS00YWZiLWEyMGYtZmE3ODE1NzdkYzZkIiwicGFnZXJlZiI6IjM3NDc3LTA5ODc2IiwiZmllbGRzIjp7ImFtb3VudCI6MCwiRmlyc3ROYW1lIjoiSnVsaWEiLCJMYXN0TmFtZSI6IkluZ3JhbSJ9fQ.o_oA7ImQ38A-HSbfjJaOumOaSBvZpPYChFAD1EnJyYM
As a POST form field
<form action="https://pay.segpay.com/37477-09876" method="POST">
<input type="hidden" name="jwt" value="eyJ0eXAi...SIGNATURE">
<!-- any unsigned fields go here as additional inputs -->
</form>
Choosing between GET and POST
Both transports are functionally equivalent for an initial pay page request. The differences only emerge in two edge cases: browser refresh behavior and URL length.
Browser refresh on POST
When you render the pay page via POST, the signed token is in the request body, not the URL. If the cardholder refreshes the browser, the page reloads as a GET request (or the browser prompts to resend form data, which most users decline). Either way, the token is not re-sent. The pay page treats the refreshed request as an expired session and shows an error message.
If cardholder re-entry (back button, refresh) is a common flow in your checkout, use GET so the token is preserved in the URL.
URL length on GET
Signed tokens with many fields or long field values can produce URLs that exceed browser and proxy limits (typically around 2,000 characters for the full URL). If your token is large, POST avoids the URL-length risk but commits you to the refresh behavior described above.
Code Examples
The examples use the pay page reference 37477-09876. Replace the URL, signing key, pageref, and field values with your own.
Node.js
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const signingKey = process.env.SEGPAY_SIGNING_KEY;
const pageref = '37477-09876';
const payload = {
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (30 * 60),
jti: crypto.randomUUID(),
pageref: pageref,
fields: {
amount: 14.00
}
};
// signingKey is the base64 string from Segpay - pass as-is, do NOT decode it first
const token = jwt.sign(payload, signingKey, { algorithm: 'HS256' });
const payPageUrl = `https://pay.segpay.com/${pageref}?jwt=${token}`;
Python
import jwt
import time
import uuid
import os
signing_key = os.environ['SEGPAY_SIGNING_KEY']
pageref = '37477-09876'
payload = {
'iat': int(time.time()),
'exp': int(time.time()) + 1800,
'jti': str(uuid.uuid4()),
'pageref': pageref,
'fields': {
'amount': 14.00
}
}
# signingKey is the base64 string from Segpay - pass as-is, do NOT decode it first
token = jwt.encode(payload, signing_key, algorithm='HS256')
pay_page_url = f'https://pay.segpay.com/{pageref}?jwt={token}'
PHP
<?php
use Firebase\JWT\JWT;
$signingKey = getenv('SEGPAY_SIGNING_KEY');
$pageref = '37477-09876';
$payload = [
'iat' => time(),
'exp' => time() + 1800,
'jti' => bin2hex(random_bytes(16)),
'pageref' => $pageref,
'fields' => ['amount' => 14.00],
];
// $signingKey is the base64 string from Segpay - pass as-is, do NOT decode it first
$token = JWT::encode($payload, $signingKey, 'HS256');
$payPageUrl = "https://pay.segpay.com/{$pageref}?jwt={$token}";
Test Vectors
Use these test vectors to verify your signing implementation produces correct output. The inputs below produce a known-good signed token. If your code produces the same output from the same inputs, your HMAC routine is correctly configured.
Test vector 1: amount-only token
Inputs:
Signing key: EXAMPLE000000000000000000000000000000000000=
Header: {"typ":"JWT","alg":"HS256"}
Payload: {"iat":1700000000,"exp":1700001800,"jti":"00000000-0000-4000-8000-000000000001","pageref":"acme-checkout-1","fields":{"amount":"29.99"}}
Expected output (full signed token):
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMTgwMCwianRpIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicGFnZXJlZiI6ImFjbWUtY2hlY2tvdXQtMSIsImZpZWxkcyI6eyJhbW91bnQiOiIyOS45OSJ9fQ.ZWQ54K_a4GjpKnAdViZ9V4cJ9adu_Lf7bmCkiJL9blA
| Output Section | Value |
|---|---|
| Header (base64url-encoded) | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
|
| Payload (base64url-encoded) | eyJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMTgwMCwianRpIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicGFnZXJlZiI6ImFjbWUtY2hlY2tvdXQtMSIsImZpZWxkcyI6eyJhbW91bnQiOiIyOS45OSJ9fQ
|
| Signature (base64url-encoded) | ZWQ54K_a4GjpKnAdViZ9V4cJ9adu_Lf7bmCkiJL9blA
|
Note on byte-exact matching: The base64-encoded payload depends on the exact JSON serialization (key order, whitespace, and so on). If your code serializes the payload differently (for example, by alphabetizing keys instead of using insertion order), you will get a different payload segment and therefore a different signature. This is expected and your token will still verify correctly on the gateway. If you need to match the test vector exactly, serialize the payload exactly as shown: keys in iat, exp, jti, pageref, fields order, no white space, and no trailing new line.
Testing your integration
Segpay doesn't use a separate sandbox URL. You integrate using the same pay.segpay.com domain in both test and production phases. Your Segpay Merchant Services contact provides a demo MID for testing. Transactions made using the demo MID go through the full pay page flow but don't post to the bank. When you're ready to launch, you can start using your live MID.
Verifying the integration
The Segpay Gateway admin includes a built-in Test feature that generates a valid signed request for any configured pay page. The URL it produces matches the format your code should generate, serving as a useful reference for comparison. Ask your Merchant Services contact to run it and share the resulting URL.
You can also decode any JWT you produce at JWT.IO to inspect its structure before sending. Paste your token in the left panel; the decoded header and payload appear on the right. (Don't paste production signing keys into JWT.IO . Only use it for inspecting the payload.)
Error handling
Verification failure
If a JWT fails verification (invalid signature, expired, missing required claim, wrong algorithm), the pay page displays an error page rather than loading. The customer sees:
"This transaction cannot be completed at this time. Please contact the website where you attempted the purchase for further assistance, and reference code: <reference-code>"
Capture the reference code and contact Segpay Merchant Services with it. Segpay uses the code to locate the exact failure in the application logs.
Common failures:
| Symptom | Likely Cause |
|---|---|
| Pay page rejects all requests | Signing key not updated after a rotation. |
| Pay page rejects intermittently | Token being cached or reused past its 30-minute expiration. |
| Pay page rejects the same valid token a second time | Each token has a single-use jti. Generate a fresh token per checkout. |
| Specific field doesn't update | Field is configured as Require Signing but not included in the fields object. |
| All requests fail right after a deploy | Wrong algorithm (must be HS256), or wrong key environment. |
Session expired
A session-expired failure happens when the signed token was previously valid and the cardholder reached the pay page successfully, but the session has since lapsed. Common causes:
-
The cardholder sat on the pay page past the 30-minute token expiration.
-
The cardholder abandoned the page and returned to it later.
-
The cardholder hit browser refresh on a POST-rendered page (the token is in the request body, not the URL, so refresh drops it).
-
The cardholder sees a session-expired message and is directed to return to the merchant's site to try again.
Postback results
Unlike verification failures, session-expired events do trigger a postback to your configured postback URL. The postback contains:
| Field | Value |
|---|---|
ResponseCode
|
804
|
ResponseCodeDescription
|
Transaction timeout
|
TransactionID
|
jwt-<jti> (the jwt- prefix distinguishes these from real transaction IDs) |
Treat session-expired as "cardholder abandoned payment." There is no continuation from the original session. The cardholder needs to start a fresh checkout on your site.
Key rotation
Recommended rotation flow:
Each pay page supports up to two active signing keys simultaneously to enable zero-downtime rotation.
-
Request a new key from your Segpay Merchant Services contact.
-
Deploy the new key to your servers alongside the existing one.
-
Switch your JWT signing code to use the new key.
-
Once you've verified all traffic succeeding with the new key, ask Segpay Merchant Services to remove the old key.
If you believe a key has been compromised, contact Merchant Services immediately to rotate it.
Support
For integration questions, contact Segpay Technical Support with:
-
The pay page reference (
pageref). -
The link or URL you're using when the issue occurs.
-
A sample JWT. Provide the decoded payload (use https://jwt.io to decode).
-
Any reference codes from error pages encountered during testing.
-
A description of how you're filling out the fields when the issue occurs.
-
Optional: a recorded video (Loom or similar) showing the issue, if it's easier to demonstrate than describe.