Skip to main content

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.

note

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):

HeaderValuePurposeRequired?
X-API-Key-IDPublic identifier (safe to log)Server-side secret lookupAll
X-TimestampCurrent unix seconds (UTC)Replay protection — 5-minute (300-second) windowAll
X-Signaturehmac-sha256=<hex>Integrity + authAll
X-Request-IDUUID v4Distributed tracingAll

Scheme B — Event Receiver (/psp/v1/events, /psp/v1/settlements via payment_service:8085):

HeaderValuePurposeRequired?
X-PSP-Key-IDPublic identifier (safe to log)Server-side secret lookupAll
X-PSP-TimestampRFC3339 timestamp (e.g. 2026-05-23T10:02:15Z)Replay protection — 5-minute windowAll
X-PSP-Signature<hex> (plain hex, no prefix)Integrity + authAll
X-Request-IDUUID v4Distributed tracingAll
X-Idempotency-KeyUUID v4 (caller-generated, per-event)Idempotent event delivery — server returns HTTP 200 with "status": "duplicate_skipped" if seen beforePOST /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

Scheme A and Scheme B use different string-to-sign formats

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), matching X-Timestamp
  • METHOD — uppercase (GET, POST, etc.)
  • path+query — full path + query string. Do not include the scheme or host
  • body — 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 from X-PSP-Timestamp header (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",...}
note

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

  1. Timestamp window: 5 minutes (300 seconds). If server clock skew exceeds that, sync via NTP.
  2. Replay protection: timestamp window (5-minute) prevents replayed signatures. For event endpoints (/psp/v1/events), the external_event_id field in the request body is checked against psp.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. The X-Idempotency-Key header is required by protocol convention and should match external_event_id.
  3. Constant-time compare: the server uses hmac.Equal — not vulnerable to timing attacks.
  4. 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.
  5. Never log the secret: log only X-API-Key-ID, X-Timestamp, and X-Signature (already hashed). Never log KESLES_HMAC_SECRET in 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