Skip to content

Authentication

Every merchant API request carries an HMAC-SHA256 signature. There is no bearer token: possession of the secret is proven per request, so a captured request cannot be replayed against a different path, method or body.

If you use @flowpayra/sdk this is handled for you. Read on if you are signing from another language.

Header Value
x-api-key Your key id
x-timestamp Unix seconds
x-nonce Unique per request
x-signature Lowercase hex HMAC-SHA256

The timestamp must be within 300 seconds of server time. A nonce is accepted once — replaying it fails even inside the window.

Six fields joined by newlines, in this order:

{key_id}\n{timestamp}\n{nonce}\n{METHOD}\n{path_and_query}\n{sha256_hex(body)}

Two details cause most integration failures:

  • path_and_query includes the query string. Sign /v1/payments?limit=10, not /v1/payments. The signature covers the query so it cannot be rewritten in transit.
  • The body hash is always present. For a request with no body, hash the empty string — e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

The signature is hex(hmac_sha256(secret_utf8, canonical_utf8)).

This vector is one of the shared fixtures the server and the TypeScript SDK are both tested against. If you reproduce it byte for byte, your signing is correct.

secret sk_demo_secret
key_id ak_demo
timestamp 1700000000
nonce 0f1e2d3c4b5a69788796a5b4c3d2e1f0
method POST
path /v1/payments
body {"merchant_order_no":"order-1","quotes":{"cur_usdc":{"amount":"100","chain_ids":["chain_base"]}}}
body_sha256 4d86aac4754077a8f1e255cae0c7d87cae3c9ef1d5dd71d14d99c17d77833ca1
signature 8daf2e606f510f1df1a1efe39761a9ac561330ce598a3b9eb88760941eaba128

A key is either read or trade. A read key is rejected with 403 on any method other than GET, so a key that only needs to poll payment status cannot create or cancel one. Issue the narrowest scope that does the job.