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.- TypeScript
- Laravel
.env
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.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.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.X-Fetchski-Content-SHA256:
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.