Covfefe

Webhooks

Signed HTTP POSTs for every Trump post: payload reference, signature verification, retries, and delivery guarantees.

Setting up

  1. Choose the Webhooks plan on covfefe.app. After checkout you'll see your API key (it's also emailed to you).
  2. Sign in to Webhook settings with the API key.
  3. Enter your endpoint URL, copy your signing secret, and click Send test event to check everything works.

Endpoint URLs must use https:// and point at a public host. Addresses on private networks (like localhost, 10.x.x.x or *.internal) are rejected. To experiment before you have an endpoint, use a free inspector like webhook.site. To receive events on your own machine, see Testing on your own machine.

Events

Each post produces:

Event When enrichment
post.created Within about a second of detecting the post null
post.enriched About 2 seconds later Score, topics, entities, summary
post.enriched (again) Video posts only: once Truth Social has made the video's preview frame, usually minutes later Updated with what the frame shows

If scoring fails, post.enriched isn't sent for that post. Treat the latest post.enriched for a post as current.

Request format

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Covfefe-Webhooks/1.0 (+https://covfefe.app)
X-Covfefe-Event: post.enriched
X-Covfefe-Signature: t=1758844981,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
  "event": "post.enriched",
  "post": {
    "id": "117334093403653279",
    "created_at": "2026-09-25T22:43:01.088Z",
    "url": "https://truthsocial.com/@realDonaldTrump/117334093403653279",
    "content": "Tariffs on Chinese semiconductors start Monday!",
    "media": ["https://static-assets-1.truthsocial.com/.../chart.jpg"],
    "retruth_of": null,
    "type": "image"
  },
  "detected_at": "2026-09-25T22:43:09.960Z",
  "delayed": false,
  "enrichment": {
    "relevance": 8,
    "topics": ["tariffs", "china"],
    "entities": {"tickers": ["NVDA"], "sectors": ["semiconductors"], "countries": ["China"], "people": []},
    "plain_english": "Announces tariffs on Chinese chip imports from Monday.",
    "media_description": "Chart of semiconductor import volumes by country."
  }
}

The actual body is compact (no whitespace), with non-ASCII characters escaped (for example “ for a curly quote).

Fields

Field Description
event post.created or post.enriched
post.id Truth Social's post ID, as a string. Use it with event to deduplicate
post.created_at When Truth Social stamped the post (UTC)
post.url Link to the post
post.content Plain-text post content, with HTML removed. For a ReTruth, the original post's text
post.media URLs of attached images and videos
post.retruth_of The original author's handle for a ReTruth, otherwise null
post.type text, image, video or retruth
detected_at When Covfefe saw the post (UTC). detected_at minus created_at is how long detection took
delayed true if the post was detected more than 10 minutes after it was made (for example after an outage)
enrichment.relevance Covfefe's automated market-relevance score, 0–10. Machine-generated, not a forecast
enrichment.topics Up to 5 short lowercase topic tags
enrichment.entities tickers (US symbols), sectors, countries, people
enrichment.plain_english One neutral sentence restating the post
enrichment.media_description What attached images or a video's preview frame show, including on-image text. Empty for text-only posts
test Present and true only on test events sent from your settings page. Real events don't include it

Verifying signatures

Every request is signed with your signing secret, so you can reject anything that didn't come from Covfefe. The header looks like t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw request body>".

Two rules:

  • Use the raw body bytes exactly as received. Don't parse the JSON and re-serialize it; that changes the bytes and the signature won't match.
  • Reject old timestamps (we suggest over 5 minutes) so a captured request can't be replayed later.

Python

import hashlib
import hmac
import time


def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
    try:
        parts = dict(item.split("=", 1) for item in header.split(","))
        timestamp = int(parts["t"])
        signature = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

With Flask, for example: verify(SECRET, request.get_data(), request.headers.get("X-Covfefe-Signature", "")).

Node.js

const crypto = require("crypto");

function verify(secret, rawBody, header, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((part) => {
      const i = part.indexOf("=");
      return [part.slice(0, i), part.slice(i + 1)];
    })
  );
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp) || typeof parts.v1 !== "string") return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex");
  return (
    expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
  );
}

With Express, read the raw body with express.raw({ type: "application/json" }) and pass req.body (a Buffer) as rawBody.

Responding, retries, and ordering

  • Respond with any 2xx status within 5 seconds. We ignore the response body. Do slow work (API calls, database writes) after responding, or on a queue.
  • Retries. A non-2xx response, a timeout, or a connection error is retried after 2, 5, 15, 30, 60 and 120 seconds (7 attempts over about 4 minutes). After that the event is dropped. Redirects aren't followed.
  • Ordering. For each post, post.created (with its retries) is delivered before its post.enriched. Events for different posts can arrive interleaved.
  • Duplicates. Deduplicate on post.id plus event, but let a newer post.enriched replace an older one, since video posts can send two.
  • Auto-disable. If your endpoint keeps failing for 3 days, deliveries are paused and your settings page shows a banner. Click Enable deliveries, or save a new URL, to resume.
  • Delivery log. Your settings page lists recent deliveries with status, attempts, and latency.

We don't send from fixed IP addresses, so verify signatures instead of allowlisting IPs.

Testing your endpoint

Send a test event

In Webhook settings, choose a sample and click Send test event. Covfefe sends it to your endpoint, signed like a real event, and shows the result on the page: the HTTP status and how long your endpoint took, or the connection error.

Sample Event Useful for testing
Scored text post (7/10) post.enriched The everyday case. Passes the n8n example filter
Alert before scoring post.created Handling enrichment: null
High-relevance post (9/10) post.enriched Tickers, sectors and countries all filled in
Low-relevance post (1/10) post.enriched Something your filters should drop
Image post (chart) post.enriched type: image, media URLs, media_description
Video post, first score post.enriched type: video, a low score and no frame description yet
Video post, rescored post.enriched The same post.id as the previous sample, with a higher score. Send both to test that your code lets the latest event win
ReTruth post.enriched type: retruth and retruth_of
Delayed post post.enriched delayed: true
  • Samples use real field formats but fictional content (for example "widgets from Examplestan"), fake post IDs, and media URLs on example.com, so a test can't be mistaken for a real post.
  • Every sample includes "test": true, so your code can recognize and skip it.
  • It's sent once and never retried, and a failing test doesn't count toward auto-disable. You can test even while deliveries are disabled.
  • You can send one every few seconds.

Testing on your own machine

Covfefe can only deliver to public HTTPS addresses, so to receive events on your laptop, expose your local server through a tunnel and save the tunnel's URL as your endpoint:

Tool Command Your endpoint URL
Tailscale Funnel tailscale funnel 3000 https://your-machine.your-tailnet.ts.net/...
ngrok ngrok http 3000 https://<random>.ngrok-free.app/...
Cloudflare Tunnel cloudflared tunnel --url http://localhost:3000 https://<random>.trycloudflare.com/...

With Tailscale, it has to be Funnel. Tailscale Serve and plain tailnet names (which resolve to private 100.x addresses) only work inside your tailnet, so Covfefe can't reach them and the settings page rejects them. Then click Send test event to check the whole path, tunnel included.

Signing a request yourself

To test your verification code without Covfefe at all, sign a sample event yourself:

SECRET='whsec_your_signing_secret'
BODY='{"event":"post.created","post":{"id":"1","created_at":"2026-09-25T22:43:01.088Z","url":"https://truthsocial.com/@realDonaldTrump/1","content":"Test","media":[],"retruth_of":null,"type":"text"},"detected_at":"2026-09-25T22:43:09.960Z","delayed":false,"enrichment":null}'
T=$(date +%s)
SIG=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST http://localhost:3000/your/path \
  -H 'Content-Type: application/json' -H 'X-Covfefe-Event: post.created' \
  -H "X-Covfefe-Signature: t=$T,v1=$SIG" --data "$BODY"

Your API key

The API key signs you in to the settings page. It doesn't sign webhooks; the signing secret does. Keep both private. If you lose your API key, email hello@covfefe.app from the address you subscribed with.