# Twin Bridge — Integration Samples

Self-contained snippets you can run locally. All files are zero-dependency
(only the language's standard library + one optional snippet for Guzzle/fetch).

## Layout

| File | What it does |
|------|--------------|
| `hmac-sign.{go,js,php}` | Builds `X-Signature` for an outbound request to Twin Bridge |
| `webhook-receiver.{go,js,php}` | Verifies `X-Signature` on incoming webhook from Twin Bridge |

## Two different signature formats

Twin Bridge uses **two** HMAC schemes — they are intentionally different so a
leaked inbound secret cannot forge outbound webhooks and vice versa.

### Outbound request (you → Twin Bridge)

```
body_hash  = sha256_hex(raw_body)
message    = timestamp + HTTP_METHOD + URL_PATH + body_hash
signature  = hmac_sha256(api_secret, message)   // hex, lowercase
```

Send with headers:
- `X-API-Key`
- `X-Signature`
- `X-Timestamp` (unix seconds, ±300s tolerance)
- `Idempotency-Key` (UUID v4, required for POST)

Use `hmac-sign.*` as reference.

### Inbound webhook (Twin Bridge → you)

```
message    = timestamp + "." + raw_body
signature  = hmac_sha256(webhook_secret, message)   // hex, lowercase
```

Arrives with headers:
- `X-Timestamp`
- `X-Signature`
- `X-Provider: twin-bridge`

Use `webhook-receiver.*` as reference.

## Quick verification

The supplied inputs in all three `hmac-sign.*` files produce the **same**
signature when fed the same timestamp — tested against the Go, Node, and PHP
implementations. If your output differs, check for:

- Trailing whitespace / newline in the body
- URL-decoded path vs. raw path (use `/v1/transactions`, not `%2Fv1%2F...`)
- Lowercased hex (don't uppercase)
- Timestamp formatted as integer string, not ISO-8601

## Replay protection

- Outbound requests: reject if `|now - timestamp| > 300s`
- Webhooks: reject if `now - timestamp > 300s` or `now - timestamp < -30s`

Your side should enforce the same window to defend against replayed captures.

## Idempotency

Every `payment.*` webhook may arrive more than once (retry on our side, 5
attempts with exponential backoff). Dedup on `(tx_id, event)` pair — store a
processed-marker in your DB and no-op on second delivery.
