Back to Blog
webhookssecurityapihmactimetopost

How to verify TimeToPost webhook signatures before trusting a delivery

T
TimeToPost Team

TL;DR: Verify TimeToPost webhooks by checking the raw body, rejecting stale timestamps, computing HMAC-SHA256 over `<t>.<raw body>`, and comparing against every `v1=` value in `TimeToPost-Signature` before you trust or process the delivery.

Set up webhook verification before you accept a delivery

TimeToPost sends signed JSON POSTs to your HTTPS endpoint, and you should verify the signature before you do anything with the payload, especially anything that mutates state. The webhook docs cover the header names, signing scheme, and retry behavior, so start there: Webhooks.

If you want the broader setup context, the API reference shows how TimeToPost treats authenticated requests and endpoint setup, which is handy when you are wiring this into the rest of your app.

Capture the raw request body and headers exactly as received

Verification needs the raw bytes of the request body, not the JSON your framework parsed and then politely reserialized like it was doing you a favor. TimeToPost’s webhook docs spell out the exact header format, including TimeToPost-Signature with its t= timestamp plus one or more v1= values. You will also see TimeToPost-Event, TimeToPost-Event-Id, TimeToPost-Delivery, and TimeToPost-Attempt.

Keep the raw body. Keep the headers exactly as delivered. Do not normalize anything before verification.

Validate the timestamp first to block replayed deliveries

The docs tell you to reject timestamps more than 5 minutes old before you trust the event. That freshness check is your first line of defense against replays, and it comes before you even glance at the business data.

The signing secret appears once when the endpoint is created or rotated, so guard it. Storing it in an environment variable is the usual sensible move, because nobody wants secrets lounging around in source code like a forgotten parking ticket.

Compute the expected HMAC over <t>.<raw body> and compare in constant time

The verification flow from the webhook docs goes like this:

  1. Parse TimeToPost-Signature.
  2. Read the timestamp from t=.
  3. Compute HMAC-SHA256 with the signing secret over <t>.<raw body>.
  4. Compare the result against every v1= value.
  5. Use constant-time comparison.

That is the whole game. TimeToPost says the signature is HMAC-SHA256(secret, "<t>.<raw body>"), hex encoded, and the docs explicitly call out constant-time comparison. The API reference gives the surrounding webhook setup context, and the webhook docs provide the exact header and signing details: Webhooks and API reference.

Pseudocode looks like this:

signature = parse(TimeToPost-Signature)
t = signature.timestamp
if timestamp is older than 5 minutes, reject
expected = HMAC-SHA256(secret, t + "." + rawBody)
for each v1 value in the header:
  if constantTimeEqual(expected, hexDecode(v1)):
    accept signature
reject

Worked example: reject a tampered payload even if the delivery looks valid

Hypothetical example, because the webhook gremlins are always trying something.

TimeToPost sends this shape:

TimeToPost-Event: post.published
TimeToPost-Event-Id: evt_3f9c1b7a5d2e48c0a1b2c3d4e5f60718
TimeToPost-Signature: t=1792866602,v1=abc123...

The receiver gets the request, but the payload is altered in transit after the signature was generated. Maybe status is changed from published to failed, or a platform list is edited. The headers still look like a real delivery, but when you recompute HMAC-SHA256 over 1792866602.<raw body>, the digest does not match any v1= value.

Result: the receiver returns a non-2xx response, the delivery is not trusted, and business logic does not run. That is exactly the point. Verify first, panic later.

After verification, deduplicate and process by event ID

TimeToPost deliveries can arrive more than once, so dedupe on TimeToPost-Event-Id before you create side effects. The docs also say ordering is not guaranteed, so do not treat arrival order like gospel. If current state matters, fetch it from the API.

The webhook docs also cover retries and delivery history, which makes them worth bookmarking instead of reading once and then pretending everything will behave forever: TimeToPost’s webhook docs.

A practical flow is:

  1. Verify the signature.
  2. Reject stale timestamps.
  3. Check whether TimeToPost-Event-Id was already processed.
  4. If new, process it.
  5. If the current state matters, fetch it from the API.

Optional FAQ

What if the signature verifies but the event is old? Reject it anyway if it is outside the freshness window.

What if the same event arrives twice? Accept only the first processed TimeToPost-Event-Id and treat later duplicates as already handled.

Put these strategies into action

TimeToPost helps you schedule content, track performance, and grow your audience, all in one place.

No card for your first post. TimeToPost posts it at your best time.