# 3. 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:

```bash
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:

```bash
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`.

```text
bodyHash      = SHA256_hex(rawRequestBody)
signedPayload = timestamp + METHOD + pathWithQueryString + bodyHash
signature     = HMAC_SHA256_hex(hmacSecret, signedPayload)
```

Rules:

- `timestamp` is the same string sent in `X-Timestamp`.
- `METHOD` is uppercase, such as `GET` or `POST`.
- `pathWithQueryString` includes the query string.
- `rawRequestBody` must 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

```ts
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.