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.
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-Signature—HMAC-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.
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
});
$url = 'https://postto.dev/api/v1/send/YOUR_TOKEN';
$body = json_encode([
'subject' => 'Contact form',
'message' => "Hello, I'd like to...",
'email' => '[email protected]',
'name' => 'Jane Smith',
]);
$ts = time();
$sig = hash_hmac('sha256', "{$ts}.{$body}", SECRET_KEY);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
"X-PostTo-Timestamp: {$ts}",
"X-PostTo-Signature: {$sig}",
],
]);
curl_exec($ch);
curl_close($ch);
$body = json_encode([
'subject' => 'Contact form',
'message' => "Hello, I'd like to...",
'email' => '[email protected]',
'name' => 'Jane Smith',
]);
$ts = now()->unix();
$sig = hash_hmac('sha256', "{$ts}.{$body}", config('services.postto.secret'));
Http::withHeaders([
'Accept' => 'application/json',
'X-PostTo-Timestamp' => $ts,
'X-PostTo-Signature' => $sig,
])->withBody($body, 'application/json')
->post('https://postto.dev/api/v1/send/YOUR_TOKEN');
import hmac, hashlib, json, time, requests
body = json.dumps({
'subject': 'Contact form',
'message': "Hello, I'd like to...",
'email': '[email protected]',
'name': 'Jane Smith',
})
ts = str(int(time.time()))
sig = hmac.new(SECRET_KEY.encode(), f'{ts}.{body}'.encode(), hashlib.sha256).hexdigest()
requests.post(
'https://postto.dev/api/v1/send/YOUR_TOKEN',
data=body,
headers={
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-PostTo-Timestamp': ts,
'X-PostTo-Signature': sig,
},
)
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