Kenal Stamps Docs

Authentication

How to authenticate API requests using HMAC-SHA256 signing.

All Partner Integration API requests are authenticated using per-integration HMAC-SHA256 request signing. There are no bearer tokens or API keys sent directly — instead, each request is signed with your apiSecret.

Required Headers

Every request must include these three headers:

HeaderTypeDescription
x-service-idUUIDYour integration instance ID
x-timestampISO-8601 stringCurrent timestamp (±5 minute tolerance)
x-signatureHex stringHMAC-SHA256 signature

Signing Algorithm

1. Compute the body hash

Hash the raw request body using SHA-256. For GET requests (no body), hash an empty string.

const bodyHash = crypto
  .createHash("sha256")
  .update(rawBodyString) // empty string "" for GET
  .digest("hex");

2. Build the canonical string

Concatenate four components separated by newlines (\n):

${method}\n${path}\n${timestamp}\n${bodyHash}
ComponentDescription
methodHTTP method in uppercase: POST, GET
pathExact request path, e.g. /api/integration/loan/submit. No query string.
timestampThe same ISO-8601 value sent in x-timestamp
bodyHashThe hex-encoded SHA-256 from step 1

3. Compute the HMAC signature

const signature = crypto
  .createHmac("sha256", apiSecret)
  .update(stringToSign)
  .digest("hex");

Complete Example

hmac-auth.ts
import crypto from "crypto";

interface SignedHeaders {
  "x-service-id": string;
  "x-timestamp": string;
  "x-signature": string;
  "Content-Type": string;
}

export function createSignedHeaders(
  integrationId: string,
  apiSecret: string,
  method: string,
  path: string,
  body: string = ""
): SignedHeaders {
  const timestamp = new Date().toISOString();

  const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
  const stringToSign = `${method}\n${path}\n${timestamp}\n${bodyHash}`;
  const signature = crypto
    .createHmac("sha256", apiSecret)
    .update(stringToSign)
    .digest("hex");

  return {
    "Content-Type": "application/json",
    "x-service-id": integrationId,
    "x-timestamp": timestamp,
    "x-signature": signature,
  };
}

// Usage
const headers = createSignedHeaders(
  "your-integration-uuid",
  "your-api-secret",
  "POST",
  "/api/integration/loan/submit",
  JSON.stringify(requestBody)
);

Timestamp Tolerance

Requests are rejected if the x-timestamp differs from the server time by more than 5 minutes. Ensure your server clock is synchronized (NTP).

Common Errors

StatusErrorCause
401Invalid signatureSignature mismatch — check your signing implementation
401Timestamp expiredx-timestamp is more than 5 minutes from server time
401Missing required headersOne of the three required headers is absent
403Integration is inactiveThe integration instance has been deactivated

Debugging tip: Log your stringToSign value and compare it byte-by-byte with what the server expects. The most common issues are: incorrect path (must not include query string), body serialization differences, and clock skew.

GET Request Signing

For GET requests (status queries, certificate downloads):

  • The body used for hashing is an empty string ("")
  • The path is the URL pathname only — no query parameters
Example: Signing a GET request
const path = "/api/integration/contracts/status"; // no ?externalReferenceId=...
const bodyHash = crypto.createHash("sha256").update("").digest("hex");
const stringToSign = `GET\n${path}\n${timestamp}\n${bodyHash}`;

Secret Rotation

You can rotate your apiSecret and webhookSecret independently via the dashboard at /dashboard/admin/integrations. After rotation:

  • The old secret is immediately invalidated
  • Update your system with the new secret before the next request
  • Consider rotating secrets periodically as a security best practice