Skip to content
Docs Signed mode (HMAC)

Docs

Signed mode (HMAC)

Token mode is enough for any HTML form — the URL itself can't be guessed or brute-forced, and that's a reasonable bar for a public web form. Signed mode adds a second layer for server-to-server integrations: proof the request really came from your server, and protection against someone capturing and replaying an old request.

Signed mode requires a secret key that must stay on your server. Never put it in browser-side JavaScript, a mobile app bundle, or anywhere else a visitor could extract it — if the secret leaks, so does the ability to forge signed submissions. Use token mode for anything a browser submits directly.

Turning it on

Switch an endpoint to signed mode from its Security settings in the dashboard. PostTo generates a secret key, shown once — store it in your server's environment/secrets, not your codebase.

How the signature is built

Two headers travel with every signed request:

  • X-PostTo-Timestamp — the current Unix timestamp, as a string.
  • X-PostTo-SignatureHMAC-SHA256(secret, "<timestamp>.<raw request body>"), hex-encoded.

PostTo recomputes the same HMAC on its end and rejects the request (invalid_signature, 401) if it doesn't match, or if the timestamp is too far from the current time — which is what stops a captured request from being replayed later. The default tolerance is 300 seconds (5 minutes); you can tighten it to 60 seconds or relax it to 900 seconds per endpoint in Security settings.

The signature covers the exact bytes of the request body. Sign the string you're about to send, not a re-serialized version of it — if your HTTP client re-encodes the body after you've computed the signature, verification will fail.

Examples

const crypto = require('crypto');
const ts = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({
  subject: 'Contact form',
  message: 'Hello, I\'d like to...',
  email: '[email protected]',
  name: 'Jane Smith',
});
const sig = crypto.createHmac('sha256', SECRET_KEY)
  .update(`${ts}.${body}`).digest('hex');

await fetch('https://postto.dev/api/v1/send/YOUR_TOKEN', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'X-PostTo-Timestamp': ts,
    'X-PostTo-Signature': sig,
  },
  body, // must be the exact bytes that were signed
});

Signed requests always receive a JSON response, regardless of any Accept header — see response format.

Try it against your own form

Create an endpoint and get a working URL in under a minute — free plan, no credit card.

Start for free