Webhooks
Signed HTTP POSTs for every Trump post: payload reference, signature verification, retries, and delivery guarantees.
Setting up
- Choose the Webhooks plan on covfefe.app. After checkout you'll see your API key (it's also emailed to you).
- Sign in to Webhook settings with the API key.
- 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 itspost.enriched. Events for different posts can arrive interleaved. - Duplicates. Deduplicate on
post.idplusevent, but let a newerpost.enrichedreplace 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.