Skip to content

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
PartDescription
HeaderAlgorithm and token type
PayloadClaims (user/client data)
SignatureCryptographic signature

The header specifies the signing algorithm and the key identifier:

json
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key_001"
}
FieldValueDescription
algRS256RSA with SHA-256 signature
typJWTToken type
kidstringKey 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 ​

ClaimTypeDescription
issstringIssuer - always https://app.retailsuccessplatform.com
audstringAudience - always *
substringSubject - the OAuth client UUID
jtistringUnique token identifier (UUID)
iatintegerIssued at (Unix timestamp)
nbfintegerNot valid before (Unix timestamp)
expintegerExpiration (Unix timestamp)
token_typestringToken type — canonical predicate. service_account for service-account tokens, user for user tokens, device for device tokens, stream_app for stream-app tokens.
client_idstringOAuth client UUID
service_namestringPrimary service for this token
scopesarrayGranted permission scopes
permissionsarraySame as scopes (for compatibility)
is_service_accountbooleanDeprecated. 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_idstringPrimary organization context (hashkey)
accessible_orgsarrayAll accessible organization hashkeys
versionintegerToken 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.

ClaimTypeDescription
channel_permissionsobjectCanonical name. Shape: { "subscribe": [...], "publish": [...], "replay": [...] }. Each list contains CommerceStream pattern strings the token may use for the corresponding action.
cs_permissionsobjectTransitional 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 ​

  1. Check expiration - exp must be in the future
  2. Verify signature - Using the public key
  3. Validate issuer - iss must be https://app.retailsuccessplatform.com
  4. 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-keys

Response:

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_00000m2N3o4P5q6

Solution: Ensure your service account has access to the target organization.

Debug Tool ​

Use jwt.io to decode and inspect tokens during development:

  1. Go to jwt.io
  2. Paste your token in the "Encoded" field
  3. View the decoded header and payload

Security

Never paste production tokens into third-party websites. Use jwt.io only for development/test tokens.


Changelog
DateChange
2026-04-25Documented 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-14Accuracy fixes and added missing content.
2026-02-27Documentation corrections.
2026-01-15Initial publication.

ShopHero CommerceCore Platform