HMAC Authentication
Certain Kesles server-to-server endpoints use HMAC-SHA256 for authentication + integrity check. This guide explains the signature pattern from the client side.
Some Partner API endpoints use Bearer JWT (see Partner API). For HMAC-signed-per-request endpoints, the URL and payload spec are shared per partner via the Partner Agreement / encrypted onboarding channel (not a public document). The partner integration types that require HMAC are determined during onboarding.
Required Headers
Every HMAC request must include 4 required headers. Kesles has two HMAC header schemes depending on the endpoint group:
Scheme A — Lookup API (/api/psp/v1/merchants/* via dashboard_api:8082):
| Header | Value | Purpose | Required? |
|---|---|---|---|
X-API-Key-ID | Public identifier (safe to log) | Server-side secret lookup | All |
X-Timestamp | Current unix seconds (UTC) | Replay protection — 5-minute (300-second) window | All |
X-Signature | hmac-sha256=<hex> | Integrity + auth | All |
X-Request-ID | UUID v4 | Distributed tracing | All |
Scheme B — Event Receiver (/psp/v1/events, /psp/v1/settlements via payment_service:8085):
| Header | Value | Purpose | Required? |
|---|---|---|---|
X-PSP-Key-ID | Public identifier (safe to log) | Server-side secret lookup | All |
X-PSP-Timestamp | RFC3339 timestamp (e.g. 2026-05-23T10:02:15Z) | Replay protection — 5-minute window | All |
X-PSP-Signature | <hex> (plain hex, no prefix) | Integrity + auth | All |
X-Request-ID | UUID v4 | Distributed tracing | All |
X-Idempotency-Key | UUID v4 (caller-generated, per-event) | Idempotent event delivery — server returns HTTP 200 with "status": "duplicate_skipped" if seen before | POST /psp/v1/events only |
The HMAC-SHA256 algorithm is the same for both schemes, but the string-to-sign format and timestamp type differ. See the Canonical String-to-Sign section for each scheme's format.
Canonical String-to-Sign
The order of fields and the timestamp type are not the same. Using the wrong format always produces a signature mismatch.
Scheme A — Lookup API
Format: {unix_seconds}\n{METHOD}\n{path+query}\n{body}
unix_seconds— integer unix timestamp (e.g.1716452580), matchingX-TimestampMETHOD— uppercase (GET,POST, etc.)path+query— full path + query string. Do not include the scheme or hostbody— raw JSON body, byte-for-byte. Empty string for GET/DELETE — trailing newline still counts
GET example:
1716452580
GET
<partner-specific-endpoint-path>?query=value
(line 4 is empty — empty body, but the newline still counts)
Scheme B — Event Receiver
Format: {METHOD}\n{path}\n{RFC3339_timestamp}\n{body}
METHOD— uppercase (POST)path— path only, no query string (event endpoints have no query params)RFC3339_timestamp— the verbatim string fromX-PSP-Timestampheader (e.g.2026-05-23T10:02:15Z)body— raw JSON body, byte-for-byte
POST example:
POST
/psp/v1/events
2026-05-23T10:02:15Z
{"external_event_id":"evt-123","event_type":"transaction.success",...}
The specific endpoint paths that require HMAC are shared per partner via the Partner Agreement, not in this public documentation.
Compute the Signature
Node.js — Scheme A (Lookup)
import crypto from 'crypto';
const secret = process.env.KESLES_LOOKUP_HMAC_SECRET;
const timestamp = Math.floor(Date.now() / 1000).toString(); // unix seconds
const method = 'GET';
const path = '<endpoint-path-from-partner-agreement>';
const body = ''; // empty for GET
const stringToSign = `${timestamp}\n${method}\n${path}\n${body}`;
const signature = 'hmac-sha256=' + crypto
.createHmac('sha256', secret)
.update(stringToSign)
.digest('hex');
// Request headers:
// X-API-Key-ID: <key-id>
// X-Timestamp: <unix_seconds>
// X-Signature: hmac-sha256=<hex>
// X-Request-ID: <uuid-v4>
Node.js — Scheme B (Event Receiver)
import crypto from 'crypto';
const secret = process.env.KESLES_EVENTS_HMAC_SECRET;
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'); // RFC3339
const method = 'POST';
const path = '/psp/v1/events';
const body = JSON.stringify({ external_event_id: 'evt-123', event_type: 'transaction.success' });
const stringToSign = `${method}\n${path}\n${timestamp}\n${body}`;
const signature = crypto
.createHmac('sha256', secret)
.update(stringToSign)
.digest('hex'); // no prefix
// Request headers:
// X-PSP-Key-ID: <key-id>
// X-PSP-Timestamp: <RFC3339>
// X-PSP-Signature: <hex>
// X-Request-ID: <uuid-v4>
// X-Idempotency-Key: <uuid-v4>
Go
// Scheme A
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
signatureA := "hmac-sha256=" + hex.EncodeToString(mac.Sum(nil))
// Scheme B
mac2 := hmac.New(sha256.New, []byte(secret))
mac2.Write([]byte(stringToSign))
signatureB := hex.EncodeToString(mac2.Sum(nil)) // no prefix
Python
import hmac, hashlib
# Scheme A
sig_a = hmac.new(secret.encode(), string_to_sign.encode(), hashlib.sha256).hexdigest()
signature_a = f"hmac-sha256={sig_a}"
# Scheme B
sig_b = hmac.new(secret.encode(), string_to_sign.encode(), hashlib.sha256).hexdigest()
signature_b = sig_b # no prefix
Security Notes
- Timestamp window: 5 minutes (300 seconds). If server clock skew exceeds that, sync via NTP.
- Replay protection: timestamp window (5-minute) prevents replayed signatures. For event endpoints (
/psp/v1/events), theexternal_event_idfield in the request body is checked againstpsp.event_log— a duplicate event returns HTTP 200 with"status": "duplicate_skipped"in the body (not rejected or errored). The event is not re-processed. TheX-Idempotency-Keyheader is required by protocol convention and should matchexternal_event_id. - Constant-time compare: the server uses
hmac.Equal— not vulnerable to timing attacks. - IP allowlist: for certain server-to-server integrations, endpoints are also gated by an IP allowlist per credential. If partner IPs change, coordinate via security@kesles.com first.
- Never log the secret: log only
X-API-Key-ID,X-Timestamp, andX-Signature(already hashed). Never logKESLES_HMAC_SECRETin plain text.
Secret Rotation
- The secret is rotated by the Kesles team at most every 90 days.
- During rotation there is a 7-day grace period — both keys active simultaneously. Prepare your app for a dual-key fallback.
- If you suspect the secret is leaked, request a revoke immediately via security@kesles.com.
Next Steps
- Partner API Reference — standard Partner API endpoints
- For HMAC-based server-to-server endpoints, contact the Kesles integration team via the Partner Agreement