3. Authentication & HMAC Signing
3. Authentication & HMAC Signing
Authentication & HMAC Signing
Server-to-server endpointsrequests requiremust threebe headerssigned. onBrowser every request. Browser/widget endpoints requirerequests only one.need Geta thesepublishable wrong and the gateway returns 401 Unauthorized.key.
RequiredWhich headersauthentication do I use?
Server-to-server (HMAC)
X-Partner-KeyBrowser sk_live_<64hex>X-TimestampX-SignatureContent-Typeendpoints
application/jsonpk_...Widget (publishable key only)
X-Partner-Key
sk_... plus HMAC
X-Partner-Key, pk_live_<64hex>X-Timestamp, X-Signature
Backend write endpoints
sk_... plus HMAC
X-Partner-Key, X-Timestamp, X-Signature
If an endpoint says it requires a secret key, keys pk_pk_...alsoare workrejected.
Make read-onlyyour HMACfirst endpointssigned ifrequest
Set signenvironment variables:
export SIR_BASE_URL="https://devapi.sirgiving.org"
export SIR_SECRET_KEY="sk_test_..."
export SIR_HMAC_SECRET="your-hmac-secret"
Call a read endpoint:
TS=$(date +%s)
PATH_="/v1/partner/users"
BODY=""
BODY_HASH=$(printf '%s' "$BODY" | shasum -a 256 | awk '{print $1}')
SIG=$(printf '%s' "${TS}GET${PATH_}${BODY_HASH}" \
| openssl dgst -sha256 -hmac "$SIR_HMAC_SECRET" -hex \
| awk '{print $2}')
curl "$SIR_BASE_URL$PATH_" \
-H "X-Partner-Key: $SIR_SECRET_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"
If the request.request Mutationsucceeds, endpointsyour decoratedkey, timestamp, @RequiresSecretKey()rejectand signature pk_keysare with 403 Forbidden.valid.
Signature algorithm
The server computes the same value and compares it to X-Signature.
bodyHash = SHA256_hex(rawRequestBody)
signedPayload = ${timestamp}${METHOD}${path}${SHA256_hex(body)}timestamp X-Signature+ METHOD + pathWithQueryString + bodyHash
signature = HMAC-SHA256_hex(HMAC_SHA256_hex(hmacSecret, signedPayload)
Components:Rules:
timestamp—is the samevaluestringassent inX-Timestampheader, as a string of digits..METHOD—isuppercaseuppercase,HTTPsuchverb:asGET,orPOST,PATCH,.DELETEpathpathWithQueryString—includes therequestoriginalUrl, includingquery string.For example/v1/partner/users?page=1&limit=20.SHA256_hex(body)rawRequestBody—mustlowercase hex SHA-256 ofbe theraw request bodyexact bytes.Forsentemptyover HTTP.
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855hmacSecretThe sk_sk_The signature is compared with constant-time crypto.timingSafeEqual.
Reference recipes
Node.js / TypeScript
helper
import crypto from 'crypto';
import axios from 'axios';
const BASE = 'https://api.sirgiving.org';
const PARTNER_KEY = process.env.SIR_SECRET_KEY!; // sk_live_...
const HMAC_SECRET = process.env.SIR_HMAC_SECRET!; // 64-hexexport function sign(signSirRequest(method: string, path: string, body?: object)unknown) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const rawBody = body === undefined ? '' : JSON.stringify(body) : '';
const bodyHash = crypto.createHash('sha256').update(rawBody).digest('hex');
const signedPayload = `${timestamp}${method.toUpperCase()}${path}${bodyHash}`;
const signature = crypto
.createHmac('sha256', HMAC_SECRET)process.env.SIR_HMAC_SECRET!)
.update(signedPayload)
.digest('hex');
return {
timestamp, signature, rawBody };
}
async function callPartnerApi<T>(method: string, path: string, body?: object): Promise<T> {
const { timestamp, signature, rawBody } = sign(method, path, body);
const res = await axios.request<T>({
baseURL: BASE,
url: path,
method,rawBody,
headers: {
'X-Partner-Key': PARTNER_KEY,process.env.SIR_SECRET_KEY!,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json',
},
data: rawBody || undefined,
transformRequest: [(d) => d], // do NOT let axios re-serialize — body bytes must match
});
return res.data;
}
Python
Use
the exactimportrawBodyhashlib,stringhmac,returnedjson,bytime, requests BASE = "https://api.sirgiving.org" PARTNER_KEY = os.environ["SIR_SECRET_KEY"] HMAC_SECRET = os.environ["SIR_HMAC_SECRET"] def call(method: str, path: str, body: dict | None = None): timestamp = str(int(time.time())) raw_body = json.dumps(body, separators=(",", ":")) if body else "" body_hash = hashlib.sha256(raw_body.encode()).hexdigest() signed_payload = f"{timestamp}{method.upper()}{path}{body_hash}" signature = hmac.new( HMAC_SECRET.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest() headers = { "X-Partner-Key": PARTNER_KEY, "X-Timestamp": timestamp, "X-Signature": signature, "Content-Type": "application/json", } return requests.request(method, BASE + path, headers=headers, data=raw_body or None)
curl (single call)
TS=$(date +%s)
PATH_=/v1/partner/users
BODY=''
BODY_HASH=$(printf '%s' "$BODY" | shasum -a 256 | awk '{print $1}')
SIG=$(printf '%s' "${TS}GET${PATH_}${BODY_HASH}" | \
openssl dgst -sha256 -hmac "$HMAC_SECRET" -hex | awk '{print $2}')
curl https://api.sirgiving.org$PATH_ \
-H "X-Partner-Key: $SIR_SECRET_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"
Commonthe signing pitfalls
function transformRequest/v1/partner/users/v1/partner/users?page=1hmacSecretsk_sk_pk_X-Partner-Keye3b0c442...b855getGETErrorCommon responseserrors
All return 401 Unauthorized with this JSON shape:
{ "error": "INVALID_SIGNATURE", "message": "Request signature verification failed" }
Codes you may see:
| Error |
Meaning |
|---|
INVALID_API_KEY
TIMESTAMP_EXPIRED
Timestamp missing or outside the INVALID_SIGNATURE
PARTNER_NOT_ACTIVE
Partner ACTIVECheck partner approval/status
PARTNER_SUSPENDED
Partner Idempotency
ForAction allsubmissions action submissions, supplyrequire an idempotencyKey. inUse a stable key for the bodyreal-world (maxoperation, 255such chars,as uniqueorder_98765 peror partner)volunteer_shift_abc123.
If you retry a failednetwork call times out, retry with the same key,idempotencyKey. youSIR getGiving returns the original responseresult backinstead ratherof than aissuing duplicate token grant. This is how to safely retry on network failure.rewards.