← Back to Field Notes
February 08, 2026 · 6 min read

Writing HMAC/SHA256 Signers from Scratch for Shopee and Lazada

What the marketplace docs leave out. Practical guide with code snippets that actually work in production.

hmacshopee-apilazada-apiauthentication

Every time I integrate a new marketplace’s Open API, the first real engineering task is the request signer. Shopee and Lazada both use HMAC/SHA256 — but the exact sequence of “what to sign” differs, and the docs leave out the parts that actually break you.

Here’s what I learned writing both from scratch in JavaScript.

The signed Shopee/Lazada sync feeding a live sales dashboard

Why HMAC/SHA256 Signing Exists

Public marketplace APIs need to prove three things about every incoming request:

  1. It came from an authorized partner (authentication)
  2. It hasn’t been tampered with in transit (integrity)
  3. It’s fresh, not a replay (freshness via timestamp)

HMAC (Hash-based Message Authentication Code) with SHA256 solves all three. You combine your partner secret with a deterministic string of request components, hash the result, and send the hash as a signparameter. The server does the same math and rejects anything that doesn’t match.

Shopee’s Signing Algorithm

Shopee signs: partner_id + path + timestamp + access_token + shop_id

import crypto from "node:crypto";

function signShopeeRequest({
  partnerId, path, accessToken, shopId, partnerKey
}) {
  const timestamp = Math.floor(Date.now() / 1000);
  const baseString = `${partnerId}${path}${timestamp}${accessToken}${shopId}`;
  const sign = crypto
    .createHmac("sha256", partnerKey)
    .update(baseString)
    .digest("hex");
  return { timestamp, sign };
}

Gotchas:

  • timestamp is in seconds, not milliseconds (JavaScript’s Date.now() returns ms)
  • pathis the API path only — not the full URL, not the query string
  • The partner key is the HMAC key, not part of the base string

Lazada’s Signing Algorithm

Lazada is trickier. It signs all request parameters sorted alphabetically, joined as key-value pairs.

function signLazadaRequest({ params, path, appSecret }) {
  const timestamp = Date.now(); // ms here, not seconds
  const signed = { ...params, timestamp, sign_method: "sha256" };

  const sortedKeys = Object.keys(signed).sort();
  const baseString = path + sortedKeys
    .map((k) => `${k}${signed[k]}`)
    .join("");

  const sign = crypto
    .createHmac("sha256", appSecret)
    .update(baseString)
    .digest("hex")
    .toUpperCase(); // Lazada wants uppercase hex

  return { ...signed, sign };
}

Gotchas:

  • Timestamp is in milliseconds (unlike Shopee)
  • Parameters must be sorted alphabetically before concatenation
  • Hash output must be UPPERCASE hex (Shopee is lowercase)
  • Don’t include sign itself when building the base string

Common Pitfalls Both APIs Share

Timestamp drift.If your server clock is off by more than ~5 minutes from the marketplace’s, every signed request fails with a vague “invalid sign” error. Use NTP. Never trust a Docker container’s time without syncing.

URL encoding. Don’t double-encode parameters. If a value has a space, URL-encode it once in the HTTP request, but use the raw value in the signing base string. Most “invalid sign” errors I’ve debugged were encoding mismatches.

Character encoding.Use UTF-8 everywhere. If a seller name has Thai or Chinese characters, Node’s default handles it — but a system with a different default will give you silent hash mismatches.

Key rotation. Partner keys can rotate. Keep them in environment variables, never hard-coded. Store the rotation date so you can audit when signing started failing.

The Debug Loop

When signs fail, compare byte-for-byte:

  1. Log the exact base string your code generates
  2. Log the resulting hex hash
  3. Compare with the marketplace’s docs example for identical inputs
  4. If no docs example, compare against their official SDK’s output

Usually the bug is one of: wrong sort order, wrong timestamp unit, wrong case on the hex output, or an extra whitespace character sneaking into a param.

The Payoff

Once both signers work, integrating any endpoint becomes a 10-minute task. Order sync, product catalog, revenue aggregation — they all reuse the same signer. That’s what lets one person ship production integrations across three markets simultaneously.

Working on something like this?

Start a conversation →