Appearance
JWT Structure
ShopHero uses JSON Web Tokens (JWT) for authentication. Understanding the token structure helps with debugging and implementing advanced authentication scenarios.
Token Format
JWT tokens consist of three parts separated by dots:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FwcC5yZXRhaWxzdWNjZXNzcGxhdGZvcm0uY29tIiwic3ViIjoiOWExYjJjM2QtNGU1Zi02Nzg5LWFiY2QtZWYwMTIzNDU2Nzg5IiwiY2xpZW50X2lkIjoiOWExYjJjM2QtNGU1Zi02Nzg5LWFiY2QtZWYwMTIzNDU2Nzg5Iiwic2NvcGVzIjpbImVuZ2FnZWhxOioiXSwiaWF0IjoxNzA1MzI0ODAwLCJleHAiOjE3MDU0MTEyMDB9.signature| Part | Description |
|---|---|
| Header | Algorithm and token type |
| Payload | Claims (user/client data) |
| Signature | Cryptographic signature |
Header
The header specifies the signing algorithm and the key identifier:
json
{
"alg": "RS256",
"typ": "JWT",
"kid": "key_001"
}| Field | Value | Description |
|---|---|---|
alg | RS256 | RSA with SHA-256 signature |
typ | JWT | Token type |
kid | string | Key identifier — selects the public key from /.well-known/jwks.json to verify this token's signature. Required for multi-key rotation. |
Payload (Claims)
Service Account Token
Tokens issued for service accounts contain:
json
{
"iss": "https://app.retailsuccessplatform.com",
"aud": "*",
"sub": "9a1b2c3d-4e5f-6789-abcd-ef0123456789",
"jti": "a7b8c9d0-1e2f-3456-7890-abcdef012345",
"iat": 1705324800,
"nbf": 1705324800,
"exp": 1705411200,
"token_type": "service_account",
"client_id": "9a1b2c3d-4e5f-6789-abcd-ef0123456789",
"service_name": "engagehq",
"scopes": ["engagehq:content.view", "engagehq:circulars.view"],
"permissions": ["engagehq:content.view", "engagehq:circulars.view"],
"is_service_account": true,
"organization_id": "org_00000k1L2m3N4o5",
"accessible_orgs": ["org_00000k1L2m3N4o5"],
"version": 1
}Claim Definitions
| Claim | Type | Description |
|---|---|---|
iss | string | Issuer - always https://app.retailsuccessplatform.com |
aud | string | Audience - always * |
sub | string | Subject - the OAuth client UUID |
jti | string | Unique token identifier (UUID) |
iat | integer | Issued at (Unix timestamp) |
nbf | integer | Not valid before (Unix timestamp) |
exp | integer | Expiration (Unix timestamp) |
token_type | string | Token type — canonical predicate. service_account for service-account tokens, user for user tokens, device for device tokens, stream_app for stream-app tokens. |
client_id | string | OAuth client UUID |
service_name | string | Primary service for this token |
scopes | array | Granted permission scopes |
permissions | array | Same as scopes (for compatibility) |
is_service_account | boolean | Deprecated. Legacy boolean marker — use token_type === "service_account" instead. No longer emitted on newly minted tokens; in-flight tokens may still carry it until they expire. Will be removed in a future release. |
organization_id | string | Primary organization context (hashkey) |
accessible_orgs | array | All accessible organization hashkeys |
version | integer | Token version |
CommerceStream channel claims
User, service-account, and user-delegated tokens include CommerceStream channel permissions derived from the granted permission set. Each granted permission's commercestream metadata in the registry contributes patterns to these claims.
| Claim | Type | Description |
|---|---|---|
channel_permissions | object | Canonical name. Shape: { "subscribe": [...], "publish": [...], "replay": [...] }. Each list contains CommerceStream pattern strings the token may use for the corresponding action. |
cs_permissions | object | Transitional alias for channel_permissions during the migration window introduced by ADR-026. Identical value. New consumers should read channel_permissions; the alias will be removed in a future release. |
Decoding Tokens
JavaScript
javascript
function decodeToken(token) {
const [header, payload, signature] = token.split('.');
return {
header: JSON.parse(atob(header)),
payload: JSON.parse(atob(payload)),
signature: signature
};
}
// Usage
const decoded = decodeToken(accessToken);
console.log('Expires at:', new Date(decoded.payload.exp * 1000));
console.log('Scopes:', decoded.payload.scopes);PHP
php
function decodeToken(string $token): array
{
[$header, $payload, $signature] = explode('.', $token);
return [
'header' => json_decode(base64_decode($header), true),
'payload' => json_decode(base64_decode($payload), true),
'signature' => $signature
];
}
// Usage
$decoded = decodeToken($accessToken);
$expiresAt = (new DateTime())->setTimestamp($decoded['payload']['exp']);
$scopes = $decoded['payload']['scopes'];Python
python
import base64
import json
def decode_token(token: str) -> dict:
header, payload, signature = token.split('.')
# Add padding if needed
def decode_part(part):
padding = 4 - len(part) % 4
part += '=' * padding
return json.loads(base64.urlsafe_b64decode(part))
return {
'header': decode_part(header),
'payload': decode_part(payload),
'signature': signature
}
# Usage
decoded = decode_token(access_token)
expires_at = datetime.fromtimestamp(decoded['payload']['exp'])
scopes = decoded['payload']['scopes']Don't Trust Decoded Tokens
Decoding a JWT only reveals its contents. You must verify the signature to trust the data. API services handle verification automatically.
Token Verification
ShopHero APIs automatically verify tokens, but if you need to verify tokens locally:
Verification Steps
- Check expiration -
expmust be in the future - Verify signature - Using the public key
- Validate issuer -
issmust behttps://app.retailsuccessplatform.com - Check scopes - Ensure required scopes are present
Getting Public Keys
Retrieve public keys from the Identity service:
bash
curl https://identity.retailsuccessplatform.com/api/public-keysResponse:
json
{
"keys": [
{
"kid": "key_001",
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}JavaScript Verification
javascript
import jwt from 'jsonwebtoken';
async function verifyToken(token) {
// Fetch public key
const keysResponse = await fetch('https://identity.retailsuccessplatform.com/api/public-keys');
const { keys } = await keysResponse.json();
const publicKey = keys[0]; // Use key matching `kid` in token header
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://app.retailsuccessplatform.com'
});
return { valid: true, claims: decoded };
} catch (error) {
return { valid: false, error: error.message };
}
}Checking Token Expiration
Always check expiration before using a cached token:
javascript
function isTokenExpired(token, bufferSeconds = 300) {
const decoded = decodeToken(token);
const expiresAt = decoded.payload.exp * 1000; // Convert to milliseconds
const buffer = bufferSeconds * 1000;
return Date.now() >= (expiresAt - buffer);
}
// Usage
if (isTokenExpired(cachedToken)) {
cachedToken = await getNewToken();
}Token Debugging
Common Issues
Token Expired
exp: 1705324800 (2024-01-15 10:00:00)
Current: 1705411200 (2024-01-16 10:00:00)Solution: Request a new token.
Wrong Scopes
Required: engagehq:offers.view
Token scopes: ["engagehq:content.view"]Solution: Request a token with the correct scopes.
Invalid Organization
Token org_id: org_00000k1L2m3N4o5
Resource org_id: org_00000m2N3o4P5q6Solution: Ensure your service account has access to the target organization.
Debug Tool
Use jwt.io to decode and inspect tokens during development:
- Go to jwt.io
- Paste your token in the "Encoded" field
- View the decoded header and payload
Security
Never paste production tokens into third-party websites. Use jwt.io only for development/test tokens.
Related
- OAuth Token Endpoint - How to obtain tokens
- Public Keys - Key distribution for verification
- Errors - Common authentication errors
Changelog
| Date | Change |
|---|---|
| 2026-04-25 | Documented kid header (multi-key rotation per ADR-026). Marked is_service_account deprecated — token_type is the canonical predicate; the legacy boolean is no longer emitted on newly minted tokens. |
| 2026-03-14 | Accuracy fixes and added missing content. |
| 2026-02-27 | Documentation corrections. |
| 2026-01-15 | Initial publication. |