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|50Order matters, and so does the secret's position
apiKey|apiSecret|requestTime will hash cleanly and fail every time.The rules that trip people up
| Rule | Why it bites |
|---|---|
| Hash the exact string you send | If 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/Dhaka | The 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 hex | SHA-512 output should be hex-encoded. Some languages produce uppercase by default, normalise it. |
| No spaces around the pipes | String interpolation with padding is easy to do by accident, and produces a completely different hash. |
| URL-encode the query string, not the signing string | You hash the raw values, then URL-encode when assembling the URL. Hashing pre-encoded values will not match. |
| Keep the secret server-side | If 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:
- Is the field order apiKey, requestTime, apiSecret, amount?
- Are there exactly three pipes for CaaS, two for subscription?
- Any leading or trailing whitespace on a value?
- Does the
requestTimein the string match the one in your URL, byte for byte? - 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