Skip to main content
A signature proves a request came from the holder of the key’s signing secret (fss_…) and was not changed or replayed on the way. Every key gets a signing secret when it is created or rotated, shown once beside its token, and its Security tab sets which of its requests must carry a signature.

Proves the sender

A token copied from a log or a proxy is not enough on its own.

Seals the request

The method, path, query and body are all signed: change one and the signature fails.

Works once

A signed write is spent on use, and every signature expires after five minutes.

Which requests must be signed

An unsigned request that had to be signed gets 401, and the message says so. The API reference says it on each page too: the Conversion Data API’s writes carry a Signed note, and an upload a Signing note.

With the SDKs

Give the SDK the signing secret beside the key. It then signs every request it can, picks a fresh timestamp for each attempt, and signs an upload by its SHA-256. There is nothing to call.
.env
With options instead, pass signingSecret beside apiKey. Add signing: 'writes' (or 'all', the key’s mode from its Security tab) to have the client refuse a request the key can’t send before it leaves. See Configuration.

By hand

Take the time

The Unix time in seconds, as a string: 1792000000. It must be within five minutes of the site’s clock.

Build the string to sign

Join four parts with dots: the timestamp, the method in capitals, the path from /api/v1 with the query exactly as sent, and the exact body bytes (nothing for a request without a body).

Sign it

HMAC-SHA256 of that string, keyed with the whole signing secret (fss_ included), as lower-case hex.

Send three headers

Authorization: ApiKey fsk_…, X-Fetchski-Timestamp: <timestamp> and X-Fetchski-Signature: v1=<hex>. Send the body you signed, byte for byte.
These examples take the time with one-second precision. Two identical writes (same method, path and body) in the same second carry the same timestamp and signature, and the second is refused as a replay (This signature was already used). Wait for the next second before sending the same write again, or use an SDK, which signs every attempt afresh.
Sign the bytes you send, not a value you serialize again later: a re-encoded body (other spacing, other key order, escaped characters) no longer matches. Build the body once, sign it, send that string.

Which bodies can be signed

Signed byte for byte

JSON (application/json), a form body (application/x-www-form-urlencoded), the blog import’s Markdown (text/markdown), or no body at all.

Signed by its SHA-256

A multipart upload (multipart/form-data): see Uploads below.
Any other body on a request that must be signed gets 401 “This body can’t be signed”: the site keeps the exact bytes of those bodies only.

Uploads

A signed upload signs its SHA-256. A multipart body isn’t signed byte for byte: the request declares the hex SHA-256 of the exact body in X-Fetchski-Content-SHA256 and signs that digest in the body’s place. The site checks the signature before it reads the upload, then the bytes against the digest, and stores nothing that doesn’t match. The SDKs do this for you; Signing requests shows it by hand.
Build the multipart body once, so the bytes you hash are the bytes you send. Then sign the digest where the body would go, and send it in X-Fetchski-Content-SHA256:
With the SDKs, an upload is one call and signed for you:

When a signature is refused

Each refusal is 401, and its message says which check failed:
The playground in the API reference can’t sign. Try calls there with a key whose signing mode is Token only (see Authentication); a Conversion Data API write can’t be tried there at all.