Skip to content

Getting Started

This guide is written for beginner developers so you can send valid HMAC requests quickly.

1) Understand the two auth modes

Service auth

  • Used for machine-to-machine integration.
  • Auth format: CredentialType=service, CredentialId=<service-id>
  • Required signed headers: x-date;x-request-id;x-client-type

User auth

  • Used for tenant user requests.
  • Auth format: CredentialType=user, CredentialId=<user-email>
  • Must include X-Tenant-Id.
  • Required signed headers: x-date;x-tenant-id;x-request-id;x-client-type

2) Required headers

Every request must include:

  • Authorization
  • X-Date (ISO-8601 UTC, example: 2026-03-16T08:30:12.000Z)
  • X-Request-Id (UUID, always new per request)
  • X-Client-Type

For user auth, also include:

  • X-Tenant-Id

3) How signature generation works

The flow is always the same:

  1. Build the canonical request (format standar request).
  2. Hash the canonical request with SHA-256.
  3. Build the string to sign:
text
HMAC-SHA256
<X-Date>
<hash-canonical-request>
  1. Calculate HMAC-SHA256 with your secret key.

4) TypeScript example (ready to use)

ts
import { createHash, createHmac, randomUUID } from "node:crypto";

type CredentialType = "service" | "user";

function signedHeaders(type: CredentialType): string[] {
	return type === "user"
		? ["x-date", "x-tenant-id", "x-request-id", "x-client-type"]
		: ["x-date", "x-request-id", "x-client-type"];
}

function sign(params: {
	method: string;
	url: string;
	body: string;
	secretKey: string;
	date: string;
	requestId: string;
	clientType: string;
	tenantId?: string;
	credentialType: CredentialType;
}) {
	const u = new URL(params.url);
	const headers = signedHeaders(params.credentialType);

	const headerValue = (h: string) => {
		switch (h) {
			case "x-date":
				return params.date;
			case "x-tenant-id":
				return params.tenantId ?? "";
			case "x-request-id":
				return params.requestId;
			case "x-client-type":
				return params.clientType;
			default:
				return "";
		}
	};

	const canonicalHeaders =
		headers.map((h) => `${h}:${headerValue(h)}`).join("\n") + "\n";

	const payloadHash = createHash("sha256").update(params.body).digest("hex");

	const canonicalRequest = [
		params.method.toUpperCase(),
		u.pathname,
		u.search.slice(1),
		canonicalHeaders,
		headers.join(";"),
		payloadHash,
	].join("\n");

	const stringToSign = [
		"HMAC-SHA256",
		params.date,
		createHash("sha256").update(canonicalRequest).digest("hex"),
	].join("\n");

	return createHmac("sha256", params.secretKey)
		.update(stringToSign)
		.digest("hex");
}

const date = new Date().toISOString();
const requestId = randomUUID();

5) Service auth request example

http
Authorization: HMAC-SHA256 CredentialType=service, CredentialId=external-system-client, SignedHeaders=x-date;x-request-id;x-client-type, Signature=<hex-signature>
X-Date: 2026-03-16T08:30:12.000Z
X-Request-Id: 231b0de6-5d4b-4ad9-a7c5-03c2f0cba80f
X-Client-Type: service

6) User auth request example

http
Authorization: HMAC-SHA256 CredentialType=user, CredentialId=admin@example.com, SignedHeaders=x-date;x-tenant-id;x-request-id;x-client-type, Signature=<hex-signature>
X-Date: 2026-03-16T08:30:12.000Z
X-Tenant-Id: tenant-123
X-Request-Id: 0126f7aa-31bc-4c84-91ef-7b7c51f594f8
X-Client-Type: web

7) Pre-debug checklist

  • X-Date is valid UTC ISO-8601.
  • X-Request-Id is new for every request, including retries.
  • Canonical request headers are lowercase.
  • SignedHeaders order matches server rules exactly.
  • Canonical headers end with a newline (\n).
  • For GET/DELETE, hash the empty string "" as the body.
  • Use the new CredentialType + CredentialId format.

8) Common errors

  • 401 Invalid Authorization format -> wrong Authorization structure.
  • 401 Missing required headers -> one or more required headers are missing.
  • 401 Signature expired -> clock drift (selisih waktu) is too large.
  • 401 Invalid signature -> client canonical request differs from server.
  • 401 Replay request detected -> same request ID was reused.

Next