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 |
Always sent. 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. The header carries one t and one v1. 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.
import hmac, hashlib, time
def verify(secret: bytes, header: str, raw_body: bytes, tolerance=300) -> bool:
try:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t, sig = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > tolerance:
return False
except (ValueError, KeyError, AttributeError):
return False # a missing or malformed header fails the check
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
5xxis retried, up to five attempts in all, with the wait doubling from 2 seconds. - A
4xxis 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.