Skip to main content

PSP Integration Contract

Technical contract for PSPs integrating with the Kesles Merchant platform.

Confidential

This document is only for parties with a signed NDA with Kesles. Do not distribute.

1. Core Principles

  1. Read-only — PSPs have GET-only access to merchant data via the Lookup API. Write endpoints (NMID assignment, payment events) require Tier-1 Internal access.
  2. Data isolation — each bank only sees merchants whose NMID is issued by that acquirer. Bank A cannot see Bank B's merchants.
  3. Active-only — only merchants with status = active, deleted_at IS NULL, and nmid IS NOT NULL are returned.
  4. Idempotent — all requests are safe to retry (use X-Request-ID).
  5. Eventual consistency — data may lag by ≤30 seconds.

2. Authentication — HMAC-SHA256

Auth headers differ depending on which service you are calling:

2.1 Lookup API — dashboard_api (port 8082)

Applies to: GET /api/psp/v1/merchants/*

X-API-Key-ID: <public key identifier — safe to log>
X-Timestamp: <unix seconds, current time>
X-Signature: hmac-sha256=<computed signature>
X-Request-ID: <uuid v4 for tracing>

2.2 Events API — payment-service (port 8085)

Applies to: POST /psp/v1/events, POST /psp/v1/settlements

X-PSP-Key-ID: <public key identifier — safe to log>
X-PSP-Timestamp: <RFC3339 timestamp, current time — e.g. 2026-06-08T10:30:00+07:00>
X-PSP-Signature: <computed signature — raw hex, no prefix>
X-Request-ID: <uuid v4 for tracing>

2.3 How to Compute the Signature

Lookup API — string-to-sign: {timestamp}\n{method}\n{path}\n{body}

Example for GET /api/psp/v1/merchants/by-nmid/ID102326873XXXX:

1716452580
GET
/api/psp/v1/merchants/by-nmid/ID102326873XXXX

(Body is empty for GET; the trailing newline remains.)

Events API — string-to-sign: {method}\n{path}\n{timestamp}\n{body}

Example for POST /psp/v1/events:

POST
/psp/v1/events
2026-06-08T10:30:00+07:00
{"external_event_id":"psp-evt-abc-123","event_type":"transaction.success",...}

Important: the field order in signed_payload differs between the two services. The Lookup API uses unix seconds for {timestamp}; the Events API uses RFC3339. Use the correct format for each target endpoint.

Compute HMAC-SHA256 (same for both services — emit raw hex; only the Lookup API header prepends hmac-sha256=):

// Go
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
signature := hex.EncodeToString(mac.Sum(nil))
# Python
import hmac, hashlib
sig = hmac.new(secret.encode(), string_to_sign.encode(), hashlib.sha256)
signature = sig.hexdigest()
// Node.js
const crypto = require('crypto');
const signature = crypto.createHmac('sha256', secret).update(stringToSign).digest('hex');

2.4 Server-Side Validation

The server performs 5 checks before processing a request:

  1. Key ID (X-API-Key-ID or X-PSP-Key-ID) is valid and registered
  2. Timestamp freshness — rejected if |now - timestamp| > 300 seconds (prevents replay attacks)
  3. HMAC signature matches (constant-time comparison)
  4. Caller IP is in the allowlist (if configured)
  5. Rate limit per API Key ID

If any of these fails: the server returns 401 or 403 without specific details.

2.5 API Key Rotation

  • Rotate every 90 days — schedule is agreed on at onboarding
  • 7-day grace period — two keys are active simultaneously during rotation
  • Instant revocation via the Kesles team — any request with the old key is immediately rejected with 401
  • The secret is re-sent via a secure channel (1Password / Bitwarden), never via email

3. Base URL

Production : https://api-merchant.kesles.com
Staging : https://api-merchant-staging.kesles.com

4. Available Endpoints

4.1 Merchant Lookup (Lookup API — dashboard_api)

MethodPathDescription
GET/api/psp/v1/merchantsList all active merchants (cursor paginated)
GET/api/psp/v1/merchants/{id}Merchant detail by UUID
GET/api/psp/v1/merchants/by-nmid/{nmid}Merchant lookup by NMID (hot endpoint)
GET/api/psp/v1/merchants/searchSearch merchants by phone/email/npwp

4.2 Payment Events (Events API — payment-service)

MethodPathDescription
POST/psp/v1/eventsForward a payment event (transaction, refund, nmid-status-changed)
POST/psp/v1/settlementsForward a daily settlement batch
Endpoint Not Available to External PSPs

The following endpoints are restricted to Tier-1 Internal PSP and will return 403 for external bank PSPs:

  • PATCH /api/psp/v1/merchants/{id}/nmid-assignment

Error Responses

CodeMeaning
400Malformed request body or invalid field values
401Missing or invalid authentication headers
403Valid key but not authorized for this endpoint
429Rate limit exceeded — implement exponential backoff
500Transient infrastructure failure (DB timeout, service unavailable). PSPs MUST retry with exponential backoff for 500 responses.
Duplicate events are not an error

A duplicate external_event_id is not a 409 (or any error). The server is idempotent: it responds with HTTP 200 and body {"status":"duplicate_skipped"} (optionally a "message" field). Treat this as a successful, already-processed event — safe to ignore.

5xx retry guidance: the server returns 500 for transient infrastructure failures. Do not drop the event on 500 — queue it for retry with exponential backoff (suggested: 1s, 2s, 4s, 8s, capped at 5 minutes). Reuse the same X-Request-ID on retry so the server can deduplicate.

5. Rate Limits

EndpointLimit
GET /merchants60 req/min per API key
GET /merchants/{id}600 req/min per API key
GET /merchants/by-nmid/{nmid}1,000 req/min per API key
GET /merchants/search120 req/min per API key
POST /psp/v1/events6,000 req/min per API key
POST /psp/v1/settlements600 req/min per API key

Returns 429 if the limit is exceeded. Implement exponential backoff.

6. Standard Error Format

{
"error": "human readable message",
"code": "machine_readable_code"
}

See Error Codes for the complete list.