Skip to main content
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.

Functions

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).
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.

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:
1

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.
2

Sign them in the header

3

Send the same string as a raw body

Body type: Raw, body: {{ sign.body }}, with Content-Type: application/json set by hand.

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 styletimestamp.body, hex:
Slack style — a v0: prefix, hex, with the timestamp in its own header:
Shopify style — body only, base64, no timestamp:
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:
An HS256 JWT — assembled from base64url parts:
Use $credential(...), not $secret(...). $secret() resolves only inside a credential’s own refresher workflow and fails the step anywhere else.
AWS SigV4 signing key — four chained HMACs, each keyed on the previous digest. This is what key_encoding is for:
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 with botocore is less work.

Notes

  • Signing keys belong in credentials, 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.