# Receiving results

Everything we send goes to one HTTPS URL you register, as a `POST` with a JSON body, signed with a secret issued to you alone. We deliver only to `https://` URLs.

| Header | Value |
| --- | --- |
| `Luminetric-Event` | What this is: `statement.available`, `statement.withdrawn`, `request.failed`, `request.duplicate`, `retrieval.completed` or `retrieval.failed` |
| `Luminetric-Signature` | `t=<unix seconds>,v1=<hex>`, described below |
| `Luminetric-Delivery-Id` | A key unique to this event: the statement's `version_id`, or the event's `failure_id`, `withdrawal_id`, `duplicate_id` or `event_id` |
| `Luminetric-Statement-Id` | The `statement_id`, on statement and withdrawal events |
| `Luminetric-Schema-Version` | The contract version of the body, for example `1.0.0` |

## Verifying the signature

`v1` is the hex HMAC-SHA256, keyed with your secret, of the timestamp, a period and the raw request body, exactly as received:

```
signed_payload = t + "." + raw_body
expected       = hex(HMAC_SHA256(secret, signed_payload))
```

The key is your secret exactly as issued, as UTF-8 bytes. Compare `expected` with `v1` in constant time, and reject a request whose `t` is more than five minutes from your clock. The timestamp is inside the signed string, so an old signature cannot be replayed under a new time. Verify against the bytes you received, before parsing: re-serializing the JSON changes them.

```python
import hmac, hashlib, time

def verify(secret: bytes, header: str, raw_body: bytes, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts["t"], parts["v1"]
    if abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret, t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
```

## Responding and retries

Return any `2xx` once you have stored the event. Do the processing afterward.

- A connection failure, a timeout (30 seconds) or a `5xx` is retried, up to five attempts in all, with the wait doubling from 2 seconds.
- A `4xx` is not retried: it tells us the request itself is wrong, and sending the same bytes again cannot change that. We investigate and re-send.
- An event that is still undelivered is kept and re-sent once the problem is resolved, under its original key. Nothing is lost by an outage on your side. For a retrieval run, only its latest event is re-sent, since that is the run's outcome.

Delivery is **at least once**, so you may receive an event more than once. De-duplicate on `Luminetric-Delivery-Id`. Return `2xx` for an event type you do not recognize, and ignore it.

A summary bill is delivered parent first, then its children. If a delivery fails partway, you hold the parent and some children, and the parent's `children.expected_count` shows that more are owed.
