> ## Documentation Index
> Fetch the complete documentation index at: https://docs.olira.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Receiving deliveries

> The payload envelope, how to verify Olira-Signature, retries, and the delivery ledger.

When a subscribed [trigger](/outbound-actions/destinations) fires, Olira creates a **delivery**: one send to one destination. Webhooks receive a signed JSON POST; email destinations receive a message. Every attempt is recorded in the delivery ledger, which you can open in the Console (**Outbound actions**) or read with the SDK.

## Payload envelope

Your webhook endpoint receives a fixed envelope. The same JSON is `payload` on a delivery when you fetch it from the SDK:

```json theme={null}
{
  "id": "del_123",
  "type": "patient.state.changed",
  "created": "2026-08-12T09:14:05Z",
  "api_version": "2026-08-01",
  "data": { "...": "..." }
}
```

`type` is the trigger you subscribed to. On the ledger record the SDK returns, that field is called `trigger`; in the body your endpoint parses, it is `type`. `data` holds ids and counts for that trigger, not clinical field values:

| Trigger                                    | `data` fields                                                                                                                                                    |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `patient.state.changed`                    | `event_log_id`, `log_type`, `changed_paths`, `change_count`, `coalesced_count` (present when several updates for the same patient were folded into one delivery) |
| `log.no_state_change`                      | `event_log_id`, `log_type`                                                                                                                                       |
| `org.mapping.failed`                       | `source_subtype`, `error_code`                                                                                                                                   |
| `ingestion.completed` / `ingestion.failed` | `job_id`, `status`, `patient_count`, `record_count`, `failure_summary` (present only on partial failures)                                                        |

## Verifying the signature

Every webhook delivery carries an `Olira-Signature` header: `t=<unix_ts>,v1=<hex_hmac>`. Recompute it with your destination's signing secret and compare; this proves the request came from Olira and was not altered in transit.

Reject a missing or malformed timestamp, one too far in the past (replay), or one unreasonably far in the future (clock skew or forgery) **before** checking the signature at all. During secret rotation the header carries **two** `v1=` entries; check if any matches, don't assume there's exactly one. The timestamp is fresh on every attempt, including retries.

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import hmac
  import time


  def verify_signature(secret: str, header: str, raw_body: bytes, *, max_skew_seconds: int = 300) -> bool:
      fields = dict(part.split("=", 1) for part in header.split(",") if part.startswith("t="))
      try:
          timestamp = int(fields["t"])
      except (KeyError, ValueError):
          return False
      if abs(time.time() - timestamp) > max_skew_seconds:
          return False
      signatures = [part.split("=", 1)[1] for part in header.split(",") if part.startswith("v1=")]
      signed_payload = f"{timestamp}.".encode() + raw_body
      expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
      return any(hmac.compare_digest(expected, sig) for sig in signatures)
  ```

  ```csharp C# theme={null}
  using System.Security.Cryptography;
  using System.Text;

  static bool VerifySignature(string secret, string header, byte[] rawBody, int maxSkewSeconds = 300)
  {
      var parts = header.Split(',');
      var tPart = parts.FirstOrDefault(p => p.StartsWith("t="));
      if (tPart is null || !long.TryParse(tPart.Substring(2), out var timestamp))
          return false;
      if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > maxSkewSeconds)
          return false;

      var signatures = parts.Where(p => p.StartsWith("v1=")).Select(p => p.Substring(3));

      using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
      var signedPayload = Encoding.UTF8.GetBytes($"{timestamp}.").Concat(rawBody).ToArray();
      var expected = Convert.ToHexString(hmac.ComputeHash(signedPayload)).ToLowerInvariant();

      return signatures.Any(sig => CryptographicOperations.FixedTimeEquals(
          Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(sig)));
  }
  ```
</CodeGroup>

Rotate the secret when you need to; the old one stays valid for 24 hours so an in-progress rotation on your side never drops a delivery. [`rotate_action_destination_secret`](/reference/sdk/outbound-actions/rotate-action-destination-secret) returns the new secret once, the same way create does.

## Retries and the ledger

Failed webhook deliveries retry automatically, then stop if they keep failing. A destination that fails long enough is auto-disabled (20+ consecutive failures over 72 hours or more); re-enable it before you expect new sends.

Every attempt is a row in the delivery ledger: pending or sending, delivered, retrying, dead-lettered, skipped, or `buffered` (waiting for that destination's daily digest — that can be close to a day, not a few minutes). List and open deliveries in the Console, or with [`list_action_deliveries`](/reference/sdk/outbound-actions/list-action-deliveries) and [`get_action_delivery`](/reference/sdk/outbound-actions/get-action-delivery) (the latter includes the exact JSON that was sent).

[`redeliver_action_delivery`](/reference/sdk/outbound-actions/redeliver-action-delivery) resends the **same body** as the original, not a newly generated one. That works for up to 30 days. If the destination is disabled, re-enable it first (HTTP 409 otherwise).
