DropplinkDocsPowered bydropp.

SDK

Signing requests

Sign every request with HMAC-SHA256, with examples and test vectors.

Every /v1 request carries three headers:

X-Key-Id:    ak_3f9c1e2a7b6d4c80
X-Timestamp: 1760700000
X-Signature: 9f06876da1bac2e5f6d2ec5a57415506da03ec293a21979da8159b7adf5cbed5

The signature is:

signature = hex( HMAC_SHA256( key = UTF-8 bytes of secret,
                              msg = X-Timestamp + "\n" + hex( SHA256( raw body bytes ) ) ) )

Rules#

  • Sign the exact bytes you send. Serialize the body once, sign that buffer, send that buffer. Re-serializing after signing (pretty-printing, reordering keys) breaks the signature.
  • X-Timestamp is Unix time in seconds. Requests more than 5 minutes from server time are rejected. Milliseconds are rejected as bad_timestamp.
  • An empty body hashes as SHA256("").
  • Hex is lower-case (upper-case is accepted).
  • Send Content-Type: application/json. Bodies are capped at 64 KB.

Examples#

Kotlin (Android)
fun hex(bytes: ByteArray) = bytes.joinToString("") { "%02x".format(it) }
 
val body = json.toByteArray(Charsets.UTF_8)            // serialize once
val ts = (System.currentTimeMillis() / 1000).toString() // seconds
val bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(body))
val mac = Mac.getInstance("HmacSHA256").apply {
    init(SecretKeySpec(SDK_SECRET.toByteArray(Charsets.UTF_8), "HmacSHA256"))
}
val signature = hex(mac.doFinal("$ts\n$bodyHash".toByteArray(Charsets.UTF_8)))
// Headers: Content-Type: application/json, X-Key-Id, X-Timestamp: ts, X-Signature: signature
Swift (iOS)
import CryptoKit
 
let body = try JSONEncoder().encode(payload)            // serialize once
let ts = String(Int(Date().timeIntervalSince1970))      // seconds
let bodyHash = SHA256.hash(data: body).map { String(format: "%02x", $0) }.joined()
let key = SymmetricKey(data: Data(sdkSecret.utf8))
let mac = HMAC<SHA256>.authenticationCode(for: Data("\(ts)\n\(bodyHash)".utf8), using: key)
let signature = mac.map { String(format: "%02x", $0) }.joined()
Node.js (testing)
import { createHash, createHmac } from "node:crypto";
 
const body = Buffer.from(JSON.stringify(payload));
const ts = Math.floor(Date.now() / 1000).toString();
const bodyHash = createHash("sha256").update(body).digest("hex");
const signature = createHmac("sha256", secret).update(`${ts}\n${bodyHash}`).digest("hex");

Test vectors#

Check your signer against these before shipping. Both use the same secret.

Empty body
secret     test-secret-not-real-6Qh2v1rS0fJp8kZr3mW9yX4bN7cL5dE1
timestamp  1760700000
body       (empty)
signature  53321e4a021b5172eaf043159efd3d2bbb8bd5ac293602b6ee354832ff8b8a0c
Unicode body
secret     test-secret-not-real-6Qh2v1rS0fJp8kZr3mW9yX4bN7cL5dE1
timestamp  1760700001
body       {"name":"खरीदारी","emoji":"🛒"}
signature  85769f2fdde1d1c36d925decb78ef6dd17d701e179dd2517bd9488eaa0ba1ab1

The unicode case catches signers that hash characters instead of UTF-8 bytes.

Something unclear? Tell us.Powered bydropp.