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

# Signing requests

> HMAC signatures, digests and nonces in JSONata — for APIs that require a signed request rather than a bearer token.

Some APIs authenticate each request with a signature instead of (or as well as) a token: you HMAC a canonical string the vendor specifies — normally a timestamp joined to the request body — with a shared secret, and send the digest in a header. The signature changes on every request, and the timestamp bounds how long a captured request stays replayable.

These functions do that inside an ordinary `{{ ... }}` expression, so an HTTP step can sign its own request without a [Code step](/workflows/steps/code).

## Functions

| Function                                                          | Returns                                                                     |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `$hmac(key, message [, algorithm] [, encoding] [, key_encoding])` | Keyed digest of `message`. Defaults: `sha256`, `hex`, `utf8`.               |
| `$hash(message [, algorithm] [, encoding])`                       | Unkeyed digest. Same defaults.                                              |
| `$base64urlencode(value)` / `$base64urldecode(value)`             | Unpadded base64url, the JWT alphabet. (`$base64encode` is standard base64.) |
| `$uuid()`                                                         | A random v4 id, for schemes that require a nonce.                           |

`algorithm` is one of `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512` — dashes and case are ignored, so `SHA-256` works. `sha1` and `md5` are there because some vendors still require them.

`encoding` is `hex`, `base64` or `base64url`.

`key_encoding` says how to read the key *before* signing: `utf8` (default), `hex` or `base64`. Reach for it when a vendor issues a hex- or base64-encoded signing key — signing its printable form instead of the bytes it stands for produces a signature that never verifies — or when chaining HMACs, as AWS does.

For the timestamp these schemes want, use `{{ $floor($millis() / 1000) }}` (Unix seconds).

<Warning>
  A failed signature never evaluates to an empty string. If the key is missing, the message is not a string, or the algorithm is unknown, the step **fails** — an ordinary JSONata error would quietly produce `""`, and a request signed with an empty key looks perfectly well-formed to everything except the vendor.
</Warning>

## Sign the bytes you actually send

The signature has to cover the request body **byte for byte**. With **Body type: JSON**, the step serializes your object on the way out, so a signature computed over a string you built separately will not match what is sent.

Build the body once as a string, sign that same string, and send it with **Body type: Raw**:

<Steps>
  <Step title="Compute the parts once, in a Transform step">
    Timestamp and nonce must be *identical* in the signed string and in the headers. `$millis()` and `$uuid()` return something new on every call, so evaluating them twice gives you two different values and a signature that cannot verify.

    ```jsonc theme={null}
    // Transform step "sign"
    {
      "ts": "{{ $floor($millis() / 1000) }}",
      "body": "{{ $string({ 'order_id': n2.order_id, 'amount': n2.amount }) }}"
    }
    ```
  </Step>

  <Step title="Sign them in the header">
    ```jsonc theme={null}
    // HTTP step headers
    {
      "X-Timestamp": "{{ sign.ts }}",
      "X-Signature": "{{ $hmac($credential('acme').signing_key, sign.ts & '.' & sign.body) }}"
    }
    ```
  </Step>

  <Step title="Send the same string as a raw body">
    Body type: **Raw**, body: `{{ sign.body }}`, with `Content-Type: application/json` set by hand.
  </Step>
</Steps>

## Recipes

Each of these follows a vendor's published scheme. Check the vendor's docs for the exact canonical string — they differ in ways that matter.

**Stripe / Svix style** — `timestamp.body`, hex:

```jsonc theme={null}
"Signature": "t={{ sign.ts }},v1={{ $hmac($credential('stripe').signing_secret, sign.ts & '.' & sign.body) }}"
```

**Slack style** — a `v0:` prefix, hex, with the timestamp in its own header:

```jsonc theme={null}
"X-Slack-Request-Timestamp": "{{ sign.ts }}",
"X-Slack-Signature": "v0={{ $hmac($credential('slack').signing_secret, 'v0:' & sign.ts & ':' & sign.body) }}"
```

**Shopify style** — body only, base64, no timestamp:

```jsonc theme={null}
"X-Shopify-Hmac-Sha256": "{{ $hmac($credential('shopify').secret, sign.body, 'sha256', 'base64') }}"
```

**Twilio style** — HMAC-SHA1 over the request URL with its POST parameters appended in key order, base64. The step's final URL (after query params are merged and percent-encoded) is not available to expressions, so build the canonical string yourself in the Transform and send the same values as the body:

```jsonc theme={null}
// Transform step "sign"
{
  "canonical": "{{ 'https://example.com/hook' & 'CallSid' & n2.call_sid & 'From' & n2.from }}"
}
// HTTP step header
"X-Twilio-Signature": "{{ $hmac($credential('twilio').auth_token, sign.canonical, 'sha1', 'base64') }}"
```

**An HS256 JWT** — assembled from base64url parts:

```jsonc theme={null}
{{ (
  $h := $base64urlencode('{"alg":"HS256","typ":"JWT"}');
  $p := $base64urlencode($string({ 'iss': 'anyreach', 'iat': $floor($millis() / 1000) }));
  $h & '.' & $p & '.' & $hmac($credential('jwt').signing_key, $h & '.' & $p, 'sha256', 'base64url')
) }}
```

<Note>
  Use `$credential(...)`, not `$secret(...)`. `$secret()` resolves only inside a credential's own [refresher workflow](/workflows/credentials/refreshers) and fails the step anywhere else.
</Note>

**AWS SigV4 signing key** — four chained HMACs, each keyed on the previous digest. This is what `key_encoding` is for:

```jsonc theme={null}
{{ $hmac($hmac($hmac($hmac('AWS4' & $credential('aws').secret_key, sign.date),
   sign.region, 'sha256', 'hex', 'hex'),
   sign.service, 'sha256', 'hex', 'hex'),
   'aws4_request', 'sha256', 'hex', 'hex') }}
```

SigV4 also wants a payload hash for `x-amz-content-sha256`: `{{ $hash(sign.body) }}`.

That is the signing key only. The full scheme then signs a canonical request — method, path, sorted query string, the signed headers in lowercase order, and the payload hash — which you have to assemble in the Transform step alongside the body, since the step does not expose the URL it finally sends. If you need all of SigV4 rather than one signed header, a [Code step](/workflows/steps/code) with `botocore` is less work.

## Notes

* **Signing keys belong in [credentials](/workflows/credentials/overview)**, not pasted into a step. A credential's value is redacted out of the stored run record; a key typed into a header is not.
* **The signature itself is recorded** in the run's request log. That is intended — a signature is not secret, and seeing it is how you debug a rejected request. Redaction is exact-match and cannot follow a value through a hash, so do not build a signature over a secret you would rather not see derived.
* **Verifying an inbound signature is not supported.** These functions sign outbound requests. A trigger receives a parsed payload, not the raw bytes a signature would have to be checked against.
