3. Authentication & HMAC Signing
Authentication & HMAC Signing
Server-to-server requests must be signed. Browser widget requests only need a publishable key.
Which authentication do I use?
| Endpoint type | Key | Headers |
|---|---|---|
| Browser donation/config endpoints | pk_... |
X-Partner-Key |
| Backend read endpoints | sk_... plus HMAC |
X-Partner-Key, 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, pk_... keys are rejected.
Make your first signed request
Set environment 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 succeeds, your key, timestamp, and signature are valid.
Signature algorithm
The server computes the same value and compares it to X-Signature.
bodyHash = SHA256_hex(rawRequestBody)
signedPayload = timestamp + METHOD + pathWithQueryString + bodyHash
signature = HMAC_SHA256_hex(hmacSecret, signedPayload)
Rules:
timestampis the same string sent inX-Timestamp.METHODis uppercase, such asGETorPOST.pathWithQueryStringincludes the query string.rawRequestBodymust be the exact bytes sent over HTTP.- Empty bodies use the SHA-256 hash of the empty string.
- The timestamp must be within 5 minutes of server time.
Node.js helper
import crypto from 'crypto';
export function signSirRequest(method: string, path: string, body?: 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', process.env.SIR_HMAC_SECRET!)
.update(signedPayload)
.digest('hex');
return {
rawBody,
headers: {
'X-Partner-Key': process.env.SIR_SECRET_KEY!,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json',
},
};
}
Use the exact rawBody string returned by the signing function as the request body. Do not let your HTTP client re-serialize it after signing.
Common errors
| Error | Meaning | Fix |
|---|---|---|
INVALID_API_KEY |
Missing, malformed, inactive, expired, or wrong key | Check the key value and environment |
TIMESTAMP_EXPIRED |
Timestamp missing or outside the 5-minute window | Sync server time and regenerate signature |
INVALID_SIGNATURE |
HMAC did not match | Check path, query string, body bytes, and HMAC secret |
PARTNER_NOT_ACTIVE |
Partner status is not active | Check partner approval/status |
PARTNER_SUSPENDED |
Partner is suspended | Contact SIR Giving support |
Idempotency
Action submissions require an idempotencyKey. Use a stable key for the real-world operation, such as order_98765 or volunteer_shift_abc123.
If a network call times out, retry with the same idempotencyKey. SIR Giving returns the original result instead of issuing duplicate rewards.