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

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message`, the default import of the v7 Node SDK. Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Verify webhook signatures

> Verify the courier-signature header before trusting an event, and handle failed deliveries.

export const Endpoint = ({method, path, name, href, children, bare}) => {
  const verb = String(method || "").toUpperCase();
  const title = verb + " " + path;
  const label = children || name || path;
  if (bare) {
    return href ? <a href={href}><code>{title}</code></a> : <code>{title}</code>;
  }
  if (!href) {
    return <span className="cx-endpoint" data-method={verb} title={title}>
        <span className="cx-endpoint-label">{label}</span>
        <span className="cx-endpoint-method">{verb}</span>
      </span>;
  }
  return <a className="cx-endpoint" data-method={verb} href={href} title={title}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">{verb}</span>
    </a>;
};

Courier signs every request. Verify the signature before you trust an event.

## How it works

A webhook gets a `whsec_...` secret when you create it. Courier signs every request with HMAC-SHA256 in a `courier-signature` header:

```
courier-signature: t=1631816343012,signature=33777cdae0468ff0939b3609d02d14e6e80ca093c2ea233455f0767055218875
```

Split the header on `,` and `=` to read `t`, a millisecond timestamp, and `signature`. Compute `HMAC_SHA256(secret, "<t>.<raw_body>")` and compare it to `signature` in constant time:

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from "crypto";

  function verify(rawBody, header, secret) {
    const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${parts.t}.${rawBody}`, "utf8")
      .digest("hex");
    const digest = Buffer.from(expected, "hex");
    const provided = Buffer.from(parts.signature ?? "", "hex");

    // timingSafeEqual throws unless both buffers are the same length, so compare
    // lengths first. Without this a malformed or absent signature is an exception
    // rather than a rejection.
    return provided.length === digest.length && crypto.timingSafeEqual(digest, provided);
  }
  ```

  ```python Python theme={null}
  import hashlib
  import hmac

  def verify(raw_body: bytes, header: str, secret: str) -> bool:
      parts = dict(kv.split("=", 1) for kv in header.split(","))
      expected = hmac.new(
          secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, parts["signature"])
  ```

  ```ruby Ruby theme={null}
  require "openssl"

  def verify(raw_body, header, secret)
    parts = header.split(",").map { |kv| kv.split("=", 2) }.to_h
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{parts['t']}.#{raw_body}")
    OpenSSL.secure_compare(expected, parts["signature"])
  end
  ```

  ```go Go theme={null}
  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/hex"
  	"strings"
  )

  func verify(rawBody []byte, header, secret string) bool {
  	parts := map[string]string{}
  	for _, kv := range strings.Split(header, ",") {
  		if key, value, ok := strings.Cut(kv, "="); ok {
  			parts[key] = value
  		}
  	}
  	mac := hmac.New(sha256.New, []byte(secret))
  	mac.Write([]byte(parts["t"] + "."))
  	mac.Write(rawBody)
  	expected := hex.EncodeToString(mac.Sum(nil))
  	return hmac.Equal([]byte(expected), []byte(parts["signature"]))
  }
  ```

  ```java Java theme={null}
  import java.security.MessageDigest;
  import java.util.Arrays;
  import java.util.HexFormat;
  import java.util.Map;
  import java.util.stream.Collectors;
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;

  boolean verify(byte[] rawBody, String header, String secret) throws Exception {
      Map<String, String> parts = Arrays.stream(header.split(","))
          .map(kv -> kv.split("=", 2))
          .collect(Collectors.toMap(kv -> kv[0], kv -> kv[1]));
      Mac mac = Mac.getInstance("HmacSHA256");
      mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
      mac.update((parts.get("t") + ".").getBytes());
      String expected = HexFormat.of().formatHex(mac.doFinal(rawBody));
      return MessageDigest.isEqual(expected.getBytes(), parts.get("signature").getBytes());
  }
  ```

  ```php PHP theme={null}
  function verify(string $rawBody, string $header, string $secret): bool
  {
      parse_str(strtr($header, ',', '&'), $parts);
      $expected = hash_hmac('sha256', "{$parts['t']}.{$rawBody}", $secret);
      return hash_equals($expected, $parts['signature']);
  }
  ```

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

  static bool Verify(byte[] rawBody, string header, string secret)
  {
      var parts = header.Split(',')
          .Select(kv => kv.Split('=', 2))
          .ToDictionary(kv => kv[0], kv => kv[1]);
      using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
      var signed = Encoding.UTF8.GetBytes(parts["t"] + ".").Concat(rawBody).ToArray();
      var expected = Convert.ToHexString(hmac.ComputeHash(signed)).ToLowerInvariant();
      return CryptographicOperations.FixedTimeEquals(
          Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(parts["signature"]));
  }
  ```
</CodeGroup>

Sign against the **raw** request body, not a re-serialized object, so the bytes match what Courier hashed.

### Delivery and retries

Return a `2xx` fast and do slow work asynchronously. Courier treats a slow or failing endpoint as a failed delivery. A delivery times out after 10 seconds.

A failed delivery retries with a backoff. It starts at a few seconds, grows to 15-minute intervals, and runs for roughly a day before the event is dropped. Most `4xx` responses are non-retryable and dropped immediately.

## Limits & behavior

* **Verify against the raw body.** Re-serializing the JSON changes the bytes, so the signature will not match.
* **A destination is never auto-disabled.** Courier keeps retrying a permanently broken endpoint until each event ages out.
* **Events that age out are gone.** After roughly a day of failures the event is dropped. Backfill from the <Endpoint method="GET" path="/messages/{message_id}" name="Get message" href="/docs/api-reference/messages/get-message">Messages API</Endpoint> if you need it.

## FAQ

<AccordionGroup>
  <Accordion title="How do I know a request really came from Courier?">
    Verify the `courier-signature` header. Compute `HMAC_SHA256(secret, "<t>.<raw_body>")` with your webhook's `whsec_` secret and compare it to the header's `signature`. Reject anything that does not match.
  </Accordion>

  <Accordion title="What happens if my endpoint is down?">
    Courier retries with an increasing backoff for about a day, then drops the event. The destination is not disabled. Fix the endpoint and later events deliver normally, but events that aged out during the outage are gone.
  </Accordion>

  <Accordion title="Why does my signature check keep failing?">
    Almost always because the body was parsed and re-serialized before hashing. Hash the raw bytes of the request instead.
  </Accordion>
</AccordionGroup>
