Developer8 min read

Signatures & authentication

How to build the SHA-512 request signature, in five languages.

Code, endpoints and integration detail.

Every request to Digimart carries a signature that proves it came from you and was not modified in transit. Getting this wrong produces E1002, Invalid signature, which is comfortably the most reported integration problem.

How it works

Your API Secret never travels over the network. Instead you join a few known values together with your secret, hash the result with SHA-512, and send the hash. Digimart holds its own copy of your secret, rebuilds the same hash, and compares. If they match, the request is authentic.

The signing strings

Fields are joined with a pipe character. No spaces, no trailing separator, in exactly this order:

# Subscription, three fields
apiKey|requestTime|apiSecret

# One-time charge (CaaS), four fields, amount last
apiKey|requestTime|apiSecret|amount


# Worked example, subscription:
myApiKey123|2024-08-08T12:00:00Z|mySecretKey456

# Worked example, one-time charge of BDT 50:
myApiKey123|2024-08-08T12:00:00Z|mySecretKey456|50

Order matters, and so does the secret's position

The secret sits in the middle, not at the end. A signature built as apiKey|apiSecret|requestTime will hash cleanly and fail every time.

The rules that trip people up

Common signature mistakes
RuleWhy it bites
Hash the exact string you sendIf you format requestTime once for the hash and again for the query string, a millisecond or a timezone suffix will differ. Build it into a variable once and reuse that variable in both places.
Generate the time in Asia/DhakaThe tutorials specify the Dhaka timezone for request time generation. A server running in UTC or another zone can drift outside the accepted window and return E1011.
Send lowercase hexSHA-512 output should be hex-encoded. Some languages produce uppercase by default, normalise it.
No spaces around the pipesString interpolation with padding is easy to do by accident, and produces a completely different hash.
URL-encode the query string, not the signing stringYou hash the raw values, then URL-encode when assembling the URL. Hashing pre-encoded values will not match.
Keep the secret server-sideIf your signature is built in browser JavaScript, your secret is public and anyone can charge your customers.

Implementations

Each of these produces the one-time charge signature. Drop the final amount field for subscriptions.

import { createHash } from "node:crypto";

export function sign(apiKey, apiSecret, requestTime, amount) {
  const parts = [apiKey, requestTime, apiSecret];
  if (amount != null) parts.push(String(amount));

  return createHash("sha512")
    .update(parts.join("|"), "utf8")
    .digest("hex");
}

// Build the timestamp ONCE and pass the same value to both
// the signature and the query string.
const requestTime = new Date().toISOString();
const signature   = sign(apiKey, apiSecret, requestTime, 50);

Debugging a rejected signature

When you get E1002, print the exact signing string your code built - not the hash, and read it character by character:

  1. Is the field order apiKey, requestTime, apiSecret, amount?
  2. Are there exactly three pipes for CaaS, two for subscription?
  3. Any leading or trailing whitespace on a value?
  4. Does the requestTime in the string match the one in your URL, byte for byte?
  5. Is the hash lowercase hex, and 128 characters long?
// Log the string, not just the hash. Redact the secret in
// anything that reaches a shared log.
const signingString = `${apiKey}|${requestTime}|${apiSecret}|${amount}`;

console.log("signing string:", signingString.replace(apiSecret, "***"));
console.log("pipe count:    ", (signingString.match(/\|/g) || []).length);
console.log("signature len: ", signature.length);  // expect 128