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:
| Header | Type | Description |
|---|---|---|
x-service-id | UUID | Your integration instance ID |
x-timestamp | ISO-8601 string | Current timestamp (±5 minute tolerance) |
x-signature | Hex string | HMAC-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}| Component | Description |
|---|---|
method | HTTP method in uppercase: POST, GET |
path | Exact request path, e.g. /api/integration/loan/submit. No query string. |
timestamp | The same ISO-8601 value sent in x-timestamp |
bodyHash | The hex-encoded SHA-256 from step 1 |
3. Compute the HMAC signature
const signature = crypto
.createHmac("sha256", apiSecret)
.update(stringToSign)
.digest("hex");Complete Example
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
| Status | Error | Cause |
|---|---|---|
401 | Invalid signature | Signature mismatch — check your signing implementation |
401 | Timestamp expired | x-timestamp is more than 5 minutes from server time |
401 | Missing required headers | One of the three required headers is absent |
403 | Integration is inactive | The 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
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