{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-guides/developer/auth-and-security/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["sub-header","admonition","example-box"]},"type":"markdown"},"seo":{"title":"HMAC request signing","siteUrl":"https://docs.wise.com","projectTitle":"Wise Platform","description":"Sign file upload requests with an HMAC-SHA256 signature to prove request integrity and prevent replay attacks.","keywords":["hmac","hmac-sha256","request signing","signature","X-Signature","shared secret","file upload","replay attack"]},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"hmac-request-signing","__idx":0},"children":["HMAC request signing"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"span","attributes":{"class":"sub-header"},"children":["Sign file upload requests with a shared secret to prove request integrity."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["HMAC (hash-based message authentication code) request signing proves that a request came from you and that neither the instructions nor the uploaded file were altered in transit. You compute a signature over the request content using a secret that only you and Wise hold, and Wise recomputes it to verify the request."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Unlike ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/developer/auth-and-security/jose/jose-jws"},"children":["JOSE JWS"]},", which uses an asymmetric key pair and requires you to upload a public key, HMAC uses a single ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["symmetric shared secret"]},". This makes it simpler to set up, but means the secret must never leave your server-side environment."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["HMAC signing supplements, but does not replace, your normal authentication. Signed requests must still carry a valid ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/developer/auth-and-security/client-credentials-token"},"children":["client credentials token"]}," in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Authorization"]}," header. The signature proves integrity; the token proves identity and grants access."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"where-hmac-signing-applies","__idx":1},"children":["Where HMAC signing applies"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["HMAC signing is not enabled across the whole API. Check the API reference for the endpoint you are calling to see whether it requires HMAC signing. The endpoints that currently require it are listed in ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#how-it-works"},"children":["How it works"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If an endpoint does not require signing, sending the signature headers has no effect. If you are unsure, ask your Wise implementation team."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"how-it-works","__idx":2},"children":["How it works"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Endpoints that require HMAC signing take a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["multipart/form-data"]}," body with two parts:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["JSON part"]},": a JSON document describing what to do with the file."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["file part"]},": the file to process, such as a CSV of line items."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The name of each part depends on the endpoint:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"width":"40%","data-label":"Endpoint"},"children":["Endpoint "]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"JSON part"},"children":["JSON part"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"File part"},"children":["File part"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Used in"},"children":["Used in"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/api-reference/preview/settlement/createfundsallocationrequest"},"children":["Create settlement allocation request"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["command"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["file"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/product/send-money/funding/bring-your-own-psp"},"children":["Bring Your Own PSP (BYO-PSP)"]}]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The examples in this guide use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["command"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["file"]},", the part names for ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/api-reference/preview/settlement/createfundsallocationrequest"},"children":["Create settlement allocation request"]},". If you are calling a different endpoint, use its part names instead."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Signing covers both parts, so the instructions and the data they refer to are locked together:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Wise shares an HMAC secret with you during onboarding. Each environment has its own secret."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["For each request, you build a ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["canonical string"]}," from the request timestamp, a hash of the JSON part, and a hash of the file part."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["You compute an HMAC-SHA256 of that canonical string using the shared secret, and send it as the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}," header alongside the timestamp in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature-Timestamp"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Wise rebuilds the canonical string from the request it actually received, recomputes the HMAC with its copy of the secret, and compares the two in constant time."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["If the timestamp is stale or the signatures do not match, Wise rejects the request with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," before processing it."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"required-headers","__idx":3},"children":["Required headers"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"width":"30%","data-label":"Header"},"children":["Header "]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Required?"},"children":["Required?"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Yes"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The HMAC-SHA256 of the canonical string, ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["hex-encoded"]}," in lowercase. Always 64 hexadecimal characters."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature-Timestamp"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Yes"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The Unix timestamp in ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["seconds"]}," (not milliseconds) at which you generated the signature, as a decimal string. For example, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1700000000"]},"."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Both headers are mandatory on a signed endpoint. A request missing either one is rejected before the signature is evaluated."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"building-the-canonical-string","__idx":4},"children":["Building the canonical string"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The canonical string is three values joined by literal ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["."]}," (period) characters:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"{timestamp}.{sha256_hex(json_part)}.{sha256_hex(file_part)}\n"},"children":[]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"width":"25%","data-label":"Component"},"children":["Component "]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["timestamp"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The exact same value you send in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature-Timestamp"]}," header, as a string. Wise uses the header value to rebuild the canonical string, so any mismatch invalidates the signature."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sha256_hex(json_part)"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["SHA-256 of the raw bytes of the JSON multipart part (for example, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["command"]},"), hex-encoded in lowercase."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sha256_hex(file_part)"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["SHA-256 of the raw bytes of the file multipart part (for example, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["file"]},"), hex-encoded in lowercase."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The order is fixed: timestamp, then the JSON hash, then the file hash. The order does not depend on the part names or the order the parts appear in the request body."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning","name":"Hash the exact bytes you send"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Wise hashes the bytes of each part as they arrive on the wire. If you serialise the JSON twice — once to hash it and once to send it — any difference in key ordering, whitespace, or number formatting produces a different hash and the signature fails."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Serialise the JSON ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["once"]},", hash those bytes, and send those same bytes. The simplest way to guarantee this is to write the JSON to a file or buffer and reference it from both the hash and the request body. The same applies to the file: read it once, in binary mode, and reuse those bytes."]}]},{"$$mdtype":"Tag","name":"ExampleBox","attributes":{"title":"Canonical string example","type":"code"},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"1700000000.3f44b77e86ebb03e62eba2476ea87079e10dd8fe1ed155f4d086cf7aad236393.7ce6d2a68e9bc3fb7f8b1501e78d8f6567a7d87faa87bfe57e1544a942e84f0d\n"},"children":[]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"computing-the-signature","__idx":5},"children":["Computing the signature"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Compute ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["HMAC-SHA256(key = secret, message = canonical_string)"]}," where:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["key"]}," is the UTF-8 bytes of the shared secret exactly as Wise provided it. Do not base64-decode, hex-decode, or trim it."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["message"]}," is the UTF-8 bytes of the canonical string."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The output is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["hex-encoded in lowercase"]}," (not base64) and placed in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}," header."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use your language's standard library rather than implementing HMAC yourself. Every mainstream runtime ships a correct implementation: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hmac"]}," in Python, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["crypto.createHmac"]}," in Node.js, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["javax.crypto.Mac"]}," in Java, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hmac"]}," in Go, and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["openssl dgst -hmac"]}," on the command line."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"timestamp-tolerance-and-replay-protection","__idx":6},"children":["Timestamp tolerance and replay protection"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Wise accepts signatures whose timestamp is within ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["300 seconds (5 minutes)"]}," of Wise server time. The tolerance is symmetric, so a timestamp too far in the future is rejected just like a stale one."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This window only bounds how long a captured request can be replayed; it does not make the signature single-use unless the endpoint also tracks replays or uses idempotency."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Because of this:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Generate the timestamp immediately before sending the request, not when you build the payload."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Keep the hosts that sign requests synchronised with NTP. Clock drift beyond five minutes causes every request to fail."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Generate a fresh timestamp and signature on every retry. Re-sending a stored signature after a network timeout fails once the window has passed."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"verify-against-a-known-test-vector","__idx":7},"children":["Verify against a known test vector"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Most integration failures are canonical string mistakes rather than HMAC mistakes. Isolate the two by testing your signing function against fixed inputs before sending any HTTP request."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Feed your signing function these inputs:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"width":"25%","data-label":"Input"},"children":["Input "]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Value"},"children":["Value"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Secret"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["test-secret"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Timestamp"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1700000000"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["JSON part bytes"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"example\":\"value\"}"]}," with no trailing newline"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["File part bytes"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["id,name"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\\n"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1,test"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\\n"]}," — that is, two lines, each terminated by a single newline"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your code must produce these intermediate and final values:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"width":"25%","data-label":"Output"},"children":["Output "]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Expected value"},"children":["Expected value"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["JSON part hash"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["3f44b77e86ebb03e62eba2476ea87079e10dd8fe1ed155f4d086cf7aad236393"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["File part hash"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["7ce6d2a68e9bc3fb7f8b1501e78d8f6567a7d87faa87bfe57e1544a942e84f0d"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Canonical string"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1700000000.3f44b77e86ebb03e62eba2476ea87079e10dd8fe1ed155f4d086cf7aad236393.7ce6d2a68e9bc3fb7f8b1501e78d8f6567a7d87faa87bfe57e1544a942e84f0d"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1023c8e2a19cc42260968ab2dcf1698e8344a2939ac97f686d21aacb650273e2"]}]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Compare each intermediate value, not just the final signature — it tells you exactly where your implementation diverges:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A wrong ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["JSON part hash"]}," usually means an extra trailing newline or re-serialised JSON."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A wrong ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["file part hash"]}," usually means CRLF line endings, a missing final newline, or the file being read in text mode."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A correct ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["canonical string"]}," with a wrong signature means the secret or the output encoding is wrong."]}]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info","name":"This vector uses a test secret"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["test-secret"]}," is for offline verification of your signing function only. It will not authenticate against sandbox or production. Once your code reproduces the values above, switch to the secret Wise issued you."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"testing-with-postman","__idx":8},"children":["Testing with Postman"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Postman can sign the request for you with a pre-request script, which is the quickest way to exercise a signed endpoint by hand before writing any code."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Postman's script sandbox cannot read the bytes of a file attached to a form-data field, so the file hash has to be computed outside Postman and supplied as a variable. The script hashes the JSON part itself and reuses the precomputed file hash."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The steps below use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["command"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["file"]}," part names. For other endpoints, substitute the part names from the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#how-it-works"},"children":["table in How it works"]},", and set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["JSON_PART_NAME"]}," in the script to match."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"step-1-compute-the-file-hash","__idx":9},"children":["Step 1. Compute the file hash"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Hash your file and copy the result:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"shasum -a 256 data.csv | awk '{print $1}'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"step-2-set-up-environment-variables","__idx":10},"children":["Step 2. Set up environment variables"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create these variables in your Postman environment:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"width":"28%","data-label":"Variable"},"children":["Variable "]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"How to populate it"},"children":["How to populate it"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Hmac-secret"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The HMAC shared secret Wise issued you for the environment you are calling. Store it as a Postman ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["secret"]}," variable type so it is masked in the UI and excluded from exports."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["file-sha256"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The hash from step 1."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Hmac-timestamp"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Leave empty. The pre-request script sets this on every send."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Hmac-signature"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Leave empty. The pre-request script sets this on every send."]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"step-3-configure-the-request","__idx":11},"children":["Step 3. Configure the request"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["On your request in Postman:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Set the body to ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["form-data"]},", with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["command"]}," field as ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["text"]}," containing your JSON, and a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["file"]}," field as ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["file"]}," pointing at your file."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Add the two signature headers, referencing the variables the script populates:"]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"X-Signature: {{Hmac-signature}}\nX-Signature-Timestamp: {{Hmac-timestamp}}\n"},"children":[]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning","name":"The JSON field must be a text field"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The script reads the JSON via ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["pm.request.body.formdata.get(JSON_PART_NAME)"]},", which only returns a value for ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["text"]}," fields. If you attach the JSON as a file instead, or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["JSON_PART_NAME"]}," does not match the field name, the script silently hashes an empty string and the signature will not match."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"step-4-add-the-pre-request-script","__idx":12},"children":["Step 4. Add the pre-request script"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Paste this into the request's ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Scripts → Pre-request"]}," tab. It runs on every send, generating a fresh timestamp and signature so your request never falls outside the 300-second window."]},{"$$mdtype":"Tag","name":"ExampleBox","attributes":{"title":"Postman pre-request script","type":"code"},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"// Name of the multipart field holding the JSON part — change this to match your endpoint\nconst JSON_PART_NAME = \"command\";\n\n// Helper: SHA-256 of a String (UTF-8) or WordArray, returned as lowercase hex\nfunction sha256Hex(input) {\n    return CryptoJS.SHA256(input).toString(CryptoJS.enc.Hex);\n}\n\n// 1. Fetch secret key and precomputed file hash from environment\nconst secret = pm.environment.get(\"Hmac-secret\");\nconst fileHash = pm.environment.get(\"file-sha256\") || \"\";\n\nif (!secret) {\n    console.error(\"Missing environment variable: {{Hmac-secret}}\");\n}\nif (!fileHash) {\n    console.error(\"Missing variable: {{file-sha256}}\");\n}\n\n// 2. Generate current Unix timestamp (in seconds)\nconst timestamp = Math.floor(Date.now() / 1000).toString();\n\n// 3. Extract the JSON text from the multipart body parameters\nlet json = \"\";\nif (pm.request.body && pm.request.body.mode === \"formdata\") {\n    const jsonParam = pm.request.body.formdata.get(JSON_PART_NAME);\n    if (jsonParam) {\n        json = jsonParam;\n    }\n}\nif (!json) {\n    console.error(`Missing or empty form-data text field: ${JSON_PART_NAME}`);\n}\n\n// 4. SHA-256 (hex) of the JSON part (UTF-8 bytes)\nconst jsonHash = sha256Hex(json);\n\n// 5. Construct Canonical Data String to Sign:\n//    Format: TIMESTAMP . SHA256_HEX(JSON_PART) . SHA256_HEX(FILE_BYTES)\nconst dataToSign = `${timestamp}.${jsonHash}.${fileHash}`;\n\n// 6. Compute HMAC-SHA256 signature (hex-encoded)\nconst signature = CryptoJS.HmacSHA256(dataToSign, secret).toString(CryptoJS.enc.Hex);\n\n// 7. Save to ENVIRONMENT variables\npm.environment.set(\"Hmac-timestamp\", timestamp);\npm.environment.set(\"Hmac-signature\", signature);\n","lang":"javascript"},"children":[]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CryptoJS"]}," is available in the Postman sandbox by default; no import is needed."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"step-5-send-the-request","__idx":13},"children":["Step 5. Send the request"]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning","name":"Recompute `file-sha256` whenever the file changes"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The file hash is not derived from the file at send time. If you edit the file, regenerate the hash from step 1 and update ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["file-sha256"]},", or the signature will be computed over the old content and rejected with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Invalid signature"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This is the most common cause of signature failures when testing in Postman. If a request that previously worked starts failing, check this first."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Watch the Postman console for the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Missing environment variable"]}," warnings from the script. The script logs an error but still sends the request, so an unset secret produces a confusing ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Invalid signature"]}," rather than an obvious configuration error."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"recommended-test-cases","__idx":14},"children":["Recommended test cases"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Run these against sandbox to confirm both the happy path and your error handling:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"width":"30%","data-label":"Test case"},"children":["Test case "]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Expected result"},"children":["Expected result"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Valid signature"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The endpoint's normal success response."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Missing ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}," header"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," with detail ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Missing required X-Signature header"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Missing ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature-Timestamp"]}," header"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," with detail ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Missing required X-Signature-Timestamp header"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Tampered JSON"]}," — sign the original JSON part, then change a field value before sending"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," with detail ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Invalid signature"]},". Confirms the JSON part is covered by the signature."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Tampered file"]}," — sign the original file, then modify a row before sending"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," with detail ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Invalid signature"]},". Confirms the file is covered by the signature."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Swapped hashes"]}," — build the canonical string with the file hash before the JSON hash"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," with detail ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Invalid signature"]},". Confirms you have the order right."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Expired timestamp"]}," — sign with a timestamp 600 seconds in the past"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," with detail ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Signature timestamp expired"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Non-numeric timestamp"]}," — send ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature-Timestamp: not-a-number"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," with detail ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Invalid timestamp format"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Malformed signature"]}," — send a non-hex value in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," with detail ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Invalid signature encoding"]},"."]}]}]}]}]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info","name":"Test with a realistic file"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Hash the file from the same bytes you upload, and stream large files rather than holding them in memory twice. Testing with a production-sized file confirms your signing path does not truncate or re-encode the content."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"troubleshooting","__idx":15},"children":["Troubleshooting"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["All signature failures return ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400 Bad Request"]}," with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["detail"]}," field describing the cause. Use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["detail"]}," value to identify which check failed."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"invalid-signature","__idx":16},"children":["Invalid signature"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Wise received a well-formed signature but it did not match the request. The secret, the canonical string, or the request content differs from what you signed."]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"width":"22%","data-label":"Cause"},"children":["Cause "]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"How to diagnose"},"children":["How to diagnose"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Solution"},"children":["Solution"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Re-serialised JSON"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Check whether your code serialises the JSON separately for the hash and for the request body. Log the byte length of both and compare."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Serialise once, then reuse the same byte array for the hash and the request."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Wrong environment secret"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Confirm the secret you loaded corresponds to the environment you are calling. Compare the base URL (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["api.wise-sandbox.com"]}," vs ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["api.wise.com"]},") against the credential source."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Use each environment's own secret."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Secret transformed before use"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Check for base64 decoding, hex decoding, trimming, or unquoting applied to the secret before it is used as the HMAC key."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Use the raw UTF-8 bytes of the secret exactly as issued. Watch for trailing whitespace or newlines introduced by shell variables or secret managers."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Timestamp mismatch"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Compare the timestamp inside your canonical string with the value in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature-Timestamp"]}," header. They must be byte-identical."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Compute the timestamp once and use the same string in both places. Do not regenerate it after signing."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Hashes in the wrong order"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Check that the JSON hash comes before the file hash in your canonical string."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Use the order ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{timestamp}.{json_part}.{file_part}"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Base64 instead of hex"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Check whether ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}," is 64 lowercase hex characters. Base64 output is 44 characters and contains ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["+"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/"]},", or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["="]},"."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Hex-encode the HMAC output."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["File altered in transit"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Check whether your HTTP client re-encodes the file, for example by normalising line endings from LF to CRLF or applying a text-mode transfer."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Read and upload the file in binary mode."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Stale file hash (Postman)"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["If you are testing with the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#testing-with-postman"},"children":["Postman pre-request script"]},", re-run ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["shasum -a 256"]}," on your file and compare it against the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["file-sha256"]}," variable."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Update ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["file-sha256"]}," after every edit to the file. The script does not recompute it."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Empty JSON hash (Postman)"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Check that the JSON form-data field is a ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["text"]}," field and that its name matches ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["JSON_PART_NAME"]}," in the script. The script cannot read file-type fields and hashes an empty string if the field is missing."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Change the field type from file to text, or correct ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["JSON_PART_NAME"]},"."]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"signature-timestamp-expired","__idx":17},"children":["Signature timestamp expired"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The timestamp is more than 300 seconds away from Wise server time, in either direction."]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"width":"22%","data-label":"Cause"},"children":["Cause "]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"How to diagnose"},"children":["How to diagnose"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Solution"},"children":["Solution"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Clock drift"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Compare your signing host's clock against a reliable time source. A host running fast will also fail, because the tolerance is symmetric."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Synchronise the host with NTP."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Milliseconds instead of seconds"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A millisecond timestamp is 13 digits; a second timestamp is 10. Check the length of the value you send."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Divide by 1000, or use a seconds-based API such as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["date +%s"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["int(time.time())"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Signature generated too early"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Measure the elapsed time between generating the timestamp and sending the request. Large file uploads and batch jobs that pre-sign many requests can exceed the window."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Generate the timestamp and signature immediately before each send."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Stale signature reused on retry"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Check whether your retry logic re-sends the original headers."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Re-sign on every retry attempt with a fresh timestamp."]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"invalid-signature-encoding","__idx":18},"children":["Invalid signature encoding"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}," value is not valid hexadecimal. This is usually base64 output, an odd number of characters, or a value carrying a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sha256="]}," prefix or surrounding whitespace. Send the bare lowercase hex digest."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"invalid-timestamp-format","__idx":19},"children":["Invalid timestamp format"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature-Timestamp"]}," value is not a whole number. Send a decimal integer with no fractional part, no quotes, and no ISO 8601 formatting."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"missing-required-x-signature-header-or-missing-required-x-signature-timestamp-header","__idx":20},"children":["Missing required X-Signature header or Missing required X-Signature-Timestamp header"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The header was absent or blank. A common cause is an HTTP client that silently drops headers with empty values when your secret or timestamp variable is unset — check that both values are non-empty before sending."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"contacting-support","__idx":21},"children":["Contacting support"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When contacting Wise about an HMAC issue, include:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-trace-id"]}," from the response headers."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The HTTP status code and full response body."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The environment (sandbox or production)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The endpoint you are calling."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature-Timestamp"]}," you sent and the canonical string your code built."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Never include the shared secret or the signature itself. Contact ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"mailto:api@wise.com"},"children":["api@wise.com"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"security-best-practices","__idx":22},"children":["Security best practices"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Store the HMAC secret in a secrets manager, never in source control, configuration files, or logs."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Use separate secrets for sandbox and production."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Never log the secret, and avoid logging full signatures."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Rotate the secret periodically, and immediately if you suspect exposure. Contact your implementation manager to arrange rotation."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Sign server-side only. Because the secret is symmetric, anyone holding it can forge requests, so it must never reach client-side code or an untrusted environment."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Compare signatures in constant time if you implement HMAC verification on your own side. Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hmac.compare_digest"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["crypto.timingSafeEqual"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MessageDigest.isEqual"]},", or your platform's equivalent rather than ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["=="]},"."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"related-guides","__idx":23},"children":["Related guides"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/developer/auth-and-security/client-credentials-token"},"children":["Client credentials tokens"]}," — the authentication HMAC signing supplements."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/developer/auth-and-security/jose/jose-jws"},"children":["JOSE JWS"]}," — asymmetric request signing, for endpoints that use a key pair instead of a shared secret."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/developer/auth-and-security/mtls"},"children":["mTLS"]}," — mutual TLS for transport-level authentication."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/product/send-money/funding/bring-your-own-psp"},"children":["Bring Your Own PSP"]}," — a flow that uses HMAC-signed file uploads."]}]}]},"headings":[{"value":"HMAC request signing","id":"hmac-request-signing","depth":1},{"value":"Where HMAC signing applies","id":"where-hmac-signing-applies","depth":2},{"value":"How it works","id":"how-it-works","depth":2},{"value":"Required headers","id":"required-headers","depth":2},{"value":"Building the canonical string","id":"building-the-canonical-string","depth":2},{"value":"Computing the signature","id":"computing-the-signature","depth":2},{"value":"Timestamp tolerance and replay protection","id":"timestamp-tolerance-and-replay-protection","depth":2},{"value":"Verify against a known test vector","id":"verify-against-a-known-test-vector","depth":3},{"value":"Testing with Postman","id":"testing-with-postman","depth":3},{"value":"Step 1. Compute the file hash","id":"step-1-compute-the-file-hash","depth":4},{"value":"Step 2. Set up environment variables","id":"step-2-set-up-environment-variables","depth":4},{"value":"Step 3. Configure the request","id":"step-3-configure-the-request","depth":4},{"value":"Step 4. Add the pre-request script","id":"step-4-add-the-pre-request-script","depth":4},{"value":"Step 5. Send the request","id":"step-5-send-the-request","depth":4},{"value":"Recommended test cases","id":"recommended-test-cases","depth":3},{"value":"Troubleshooting","id":"troubleshooting","depth":2},{"value":"Invalid signature","id":"invalid-signature","depth":3},{"value":"Signature timestamp expired","id":"signature-timestamp-expired","depth":3},{"value":"Invalid signature encoding","id":"invalid-signature-encoding","depth":3},{"value":"Invalid timestamp format","id":"invalid-timestamp-format","depth":3},{"value":"Missing required X-Signature header or Missing required X-Signature-Timestamp header","id":"missing-required-x-signature-header-or-missing-required-x-signature-timestamp-header","depth":3},{"value":"Contacting support","id":"contacting-support","depth":3},{"value":"Security best practices","id":"security-best-practices","depth":2},{"value":"Related guides","id":"related-guides","depth":2}],"frontmatter":{"seo":{"description":"Sign file upload requests with an HMAC-SHA256 signature to prove request integrity and prevent replay attacks.","keywords":["hmac","hmac-sha256","request signing","signature","X-Signature","shared secret","file upload","replay attack"],"title":"HMAC request signing"},"markdown":{"toc":{"depth":3}}},"lastModified":"2026-10-07T10:23:28.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/guides/developer/auth-and-security/hmac","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}