Every V4 API request carries three headers on top of the bearer token:
| Header | Value |
|---|
X-PP-Signature | Base64 RSA-SHA256 signature (your private key) over the canonical payload. |
X-PP-Nonce | Unique random string per request. |
X-PP-Timestamp | Unix timestamp (seconds). |
The canonical payload
Both sides must build the exact same byte string, so V4 uses a versioned, key-sorted, length-prefixed form (this removes the "concatenate in the order sent" ambiguity V3 had):
v4.request|key1:len:value|key2:len:value|…
The signed fields are: all route parameters + the full request body + nonce + ts, keys sorted alphabetically. Each part is key:byte-length:value, joined with |, prefixed with v4.request|. Binding the route parameters means the path itself is tamper-proof.
Reference implementation (PHP)
function canonicalV4(string $version, array $fields): string {
ksort($fields);
$parts = [];
foreach ($fields as $k => $v) {
$s = is_scalar($v) ? (string) $v : json_encode($v);
$parts[] = $k . ':' . strlen($s) . ':' . $s;
}
return $version . '|' . implode('|', $parts);
}
$fields = $routeParams + $body; // route params + body
$fields['nonce'] = $nonce;
$fields['ts'] = (string) $ts;
$canonical = canonicalV4('v4.request', $fields);
openssl_sign($canonical, $sig, $privateKeyPem, OPENSSL_ALGO_SHA256);
$headers['X-PP-Signature'] = base64_encode($sig);
Reference implementation (Node.js)
const crypto = require('crypto');
function canonicalV4(version, fields) {
const parts = Object.keys(fields).sort().map((k) => {
const s = typeof fields[k] === 'object' ? JSON.stringify(fields[k]) : String(fields[k]);
return `${k}:${Buffer.byteLength(s)}:${s}`;
});
return `${version}|${parts.join('|')}`;
}
const fields = { ...routeParams, ...body, nonce, ts: String(ts) };
const signer = crypto.createSign('RSA-SHA256');
signer.update(canonicalV4('v4.request', fields));
const signature = signer.sign(privateKeyPem, 'base64');
Replay protection
- A reused nonce →
409 replay_detected. - A stale or future timestamp (outside the freshness TTL) →
409 replay_detected. - A tampered body or path →
401 invalid_signature.
Generate a fresh nonce and timestamp for every request, including retries of failed sends — idempotency of the operation is handled separately by the Idempotency-Key.