Guide · Webhooks
Signed webhook delivery
1322 can POST each new event for the accounts you track to your own HTTPS endpoint. Every request is signed with HMAC-SHA256 so you can prove it came from 1322 and was not changed on the way.
- Signature
X-1322-Signature- Algorithm
- HMAC-SHA256, hex
- Configure
- Settings > Webhooks
- Updated
How delivery works
- When a new event arrives for an account you track, 1322 sends one HTTPS
POSTwith a JSON body to every enabled destination you set up for that source. - It works on all eight sources: X, Truth Social, Instagram, YouTube, TikTok, Binance Square, News and FOMO. One destination covers one source; add a destination per source you want, up to 20 per account.
- A destination only receives events for what your account tracks on its source, the same list your dashboard feed uses. For FOMO that means the traders, coins and global feeds on your list, after your trade filters.
- Delivery runs on 1322's servers, so it does not need the dashboard open. It is separate from the WebSocket: use either or both.
Set up a destination
- Open Settings > Webhooks in the dashboard.
- Pick the source, enter your endpoint's
https://URL and press Add. Your account needs access to that source: an active subscription, or your own API key for it. - Copy the signing secret:
whsec_followed by 64 hex characters. It is shown once. After that the dashboard only shows a masked form. - Press Test to send one signed
test.pingevent. A test send is a single attempt with no retries, and it does not count toward auto-disable. - Use View delivery log to see the last 30 deliveries: status (delivered, pending or failed), HTTP status, number of attempts and the error, if any.
Request headers
Each request carries these headers, plus standard HTTP headers such as Host and Content-Length:
| Header | Value |
|---|---|
Content-Type | application/json |
X-1322-Event-Id | The event id. For live events it equals id in the body. Retries keep the same value. |
X-1322-Event-Type | The event type. For live events it equals type in the body; a test send uses test.ping. |
X-1322-Timestamp | Unix time in milliseconds when this attempt was signed, for example 1794312000000. New on every attempt. |
X-1322-Signature | sha256= followed by 64 lowercase hex characters. New on every attempt, because the timestamp is signed. |
POST /1322-hook HTTP/1.1
Host: your-server.example
Content-Type: application/json
X-1322-Event-Id: x:tweet:1900000000000000001
X-1322-Event-Type: tweet.update
X-1322-Timestamp: 1794312000000
X-1322-Signature: sha256=3b67daeb880cdb79200c71f459f17c3475dd5a453bcee8ea15e39d200678fa17
{"id":"x:tweet:1900000000000000001","platform":"x","type":"tweet.update","sent_at":"2026-11-10T12:00:00.000Z","event":{"id":"x:tweet:1900000000000000001","platform":"x","eventType":"tweet.update","handle":"example"}}Signature
X-1322-Signature is sha256= followed by the lowercase hex HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with your signing secret:
X-1322-Signature = "sha256=" + hex( HMAC-SHA256( key, message ) )
key = your whole signing secret as UTF-8, whsec_ prefix included
message = X-1322-Timestamp + "." + raw request body
hex = lowercase hexadecimal- Read the raw request body bytes before any JSON parsing. A body that was parsed and serialized again will not match.
- Build the message: the
X-1322-Timestampvalue exactly as sent (milliseconds, digits only), a., then the raw body. - Compute HMAC-SHA256 with your whole secret as the key,
whsec_prefix included, encode it as lowercase hex and putsha256=in front. - Compare the result with
X-1322-Signatureusing a constant-time comparison, and reject the request if they differ. - The timestamp is signed, so it cannot be changed without breaking the signature. 1322 sets no tolerance window: check the timestamp against your own tolerance if you want one.
Check your code against this test vector. It must produce the signature shown:
secret whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
timestamp 1794312000000
body {"id":"x:tweet:1900000000000000001","platform":"x","type":"tweet.update","sent_at":"2026-11-10T12:00:00.000Z","event":{"id":"x:tweet:1900000000000000001","platform":"x","eventType":"tweet.update","handle":"example"}}
signature sha256=3b67daeb880cdb79200c71f459f17c3475dd5a453bcee8ea15e39d200678fa17Verify in Node and Python
Both examples check the signature against the raw body with a constant-time comparison, answer quickly, and only then read the event.
// Node 18+, no dependencies. Run: WEBHOOK_SECRET=whsec_... node server.mjs
import { createServer } from "node:http"
import { createHmac, timingSafeEqual } from "node:crypto"
const SECRET = process.env.WEBHOOK_SECRET // the whole whsec_... value
function verify1322(rawBody, timestamp, signature) {
if (typeof timestamp !== "string" || typeof signature !== "string") return false
const expected =
"sha256=" + createHmac("sha256", SECRET).update(`${timestamp}.`).update(rawBody).digest("hex")
const a = Buffer.from(expected)
const b = Buffer.from(signature)
return a.length === b.length && timingSafeEqual(a, b)
}
createServer((req, res) => {
const chunks = []
req.on("data", (chunk) => chunks.push(chunk))
req.on("end", () => {
const rawBody = Buffer.concat(chunks) // the exact bytes 1322 signed
const ok = verify1322(rawBody, req.headers["x-1322-timestamp"], req.headers["x-1322-signature"])
if (!ok) {
res.writeHead(401).end()
return
}
// Optional: compare Number(req.headers["x-1322-timestamp"]) with Date.now()
// against a tolerance you choose. 1322 does not enforce one.
res.writeHead(204).end() // answer within 8 seconds, then do the work
const delivery = JSON.parse(rawBody.toString("utf8"))
console.log(delivery.platform, delivery.type, delivery.id)
})
}).listen(8080)# pip install flask Run: WEBHOOK_SECRET=whsec_... flask --app app run
import hashlib
import hmac
import os
from flask import Flask, abort, request
SECRET = os.environ["WEBHOOK_SECRET"].encode() # the whole whsec_... value
def verify_1322(raw_body: bytes, timestamp: str, signature: str) -> bool:
if not timestamp or not signature:
return False
mac = hmac.new(SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256)
expected = "sha256=" + mac.hexdigest()
return hmac.compare_digest(expected.encode(), signature.encode())
app = Flask(__name__)
@app.post("/1322-hook")
def hook():
raw_body = request.get_data() # the exact bytes 1322 signed
timestamp = request.headers.get("X-1322-Timestamp", "")
signature = request.headers.get("X-1322-Signature", "")
if not verify_1322(raw_body, timestamp, signature):
abort(401)
# Optional: compare int(timestamp) / 1000 with time.time() against a
# tolerance you choose. 1322 does not enforce one.
delivery = request.get_json()
print(delivery["platform"], delivery["type"], delivery["id"])
return "", 204- With Express, mount
express.raw({ type: "application/json" })on the webhook route soreq.bodyis the raw Buffer, and pass that toverify1322. - Keep the secret on your server, in an environment variable or a secret store.
Payload
The body is one JSON object:
| Field | Type | Meaning |
|---|---|---|
id | string | The event id, for example x:tweet:1900000000000000001. |
platform | string | x, truth, instagram, youtube, tiktok, binance, news or fomo. |
type | string | The event type for that source. |
sent_at | string | ISO 8601 time the body was built. Retries resend the same body, so it does not change between attempts. |
event | object | The event itself. Fields are listed below. |
event holds the fields below. A field with no value is left out, so treat every field except id, platform and eventType as optional.
| Fields | Meaning |
|---|---|
id, platform, eventType | The same values as id, platform and type in the envelope. |
account, handle, avatar, userId | Who posted: display name (the feed name for News), handle without @, avatar URL, the platform's user id. |
followers, following, verified | Account counts and the verification badge, a boolean or an object with type and label. |
content, contentType, title, sourceUrl | The text, the kind of item (post, story, reel, article, video and so on), a title for articles and videos, and the link to the original. |
media | Array of { kind, url } with kind image, video, gifv, audio or other, plus posterUrl, caption, width, height and durationS when known. |
quotedPost, replyTo, isRepost, repostOf | The quoted post, the post replied to, and repost details. |
metrics | Engagement counts when the source has them: likes, retweets, replies, quotes, views, comments, shares. |
timestamp | The event time reported by the source. |
fomo | FOMO only: kind (thesis, trade, token_verified and so on) with coin, trade, position and thesis details. |
raw | The source's own object for this event. |
This is the body the Test button sends, formatted here for reading:
{
"id": "test_1794312000000",
"platform": "x",
"type": "test.ping",
"sent_at": "2026-11-10T12:00:00.000Z",
"event": {
"id": "test_1794312000000",
"platform": "x",
"eventType": "test.ping",
"account": "Test Account",
"handle": "test",
"avatar": "",
"content": "This is a 1322 webhook test event.",
"timestamp": "2026-11-10T12:00:00.000Z"
}
}- On the wire the body is compact JSON with no extra whitespace. Always verify the bytes you received, never a re-serialized copy.
- On X, one post can arrive as several events with the same
idand a differenttype, as on the WebSocket. See additive merge in the X reference.
Timeouts and retries
- An attempt succeeds when your endpoint answers with any 2xx status within 8 seconds. The response body is ignored.
- Anything else fails the attempt: another status code (a 3xx too, since redirects are never followed), a connection or TLS error, a destination that no longer passes the rules below, or no answer within 8 seconds.
- Each event gets up to 6 attempts: the first, then retries after 1, 2, 4, 8 and 16 seconds, each wait counted from the previous failed attempt.
- A retry resends the identical body with the same
X-1322-Event-IdandX-1322-Event-Type, and a newX-1322-TimestampandX-1322-Signature. - After the sixth failed attempt the event is marked failed for that destination and is not sent again. There is no redelivery.
- Deliveries run in parallel and retries mix with new events, so events can arrive out of order. Use each event's
timestampwhen order matters. - Answer with 2xx as soon as the signature checks out, then do the slow work.
Auto-disable
- Every event that fails all 6 attempts adds one to the destination's failure count. Any successful delivery resets the count to zero.
- When the count reaches 20, the destination is disabled and stops receiving events. Settings > Webhooks shows it as disabled.
- Enabling it again there resets the count. Events from the time it was disabled are not sent later.
Destination rules
A destination URL is checked when you save it and again before every attempt.
- It must use
https://. - It must not contain a username or password.
- The host must be a public domain name or a public IP address.
localhost, names ending in.local,.internalor.localhost, and names without a dot are refused. - Every address the hostname resolves to must be public. Private, loopback, link-local, carrier-grade NAT, multicast, documentation and other reserved ranges are refused, for IPv4 and IPv6.
- The connection only uses an address that passed the check, so a DNS change after you save cannot point deliveries at an internal address.
- Redirects are never followed. A 3xx answer counts as a failed attempt.
- Each account can have up to 20 destinations.
Common questions
Which sources can deliver webhooks?
All eight: X, Truth Social, Instagram, YouTube, TikTok, Binance Square, News and FOMO. Each destination covers one source, and you need access to that source on your account.
Does 1322 reject old timestamps for me?
No. The timestamp is part of the signed message, so it cannot be changed without breaking the signature, but 1322 sets no tolerance window. Check X-1322-Timestamp (Unix milliseconds) against your own tolerance. Every attempt, retries included, is signed with a fresh timestamp.
Can the same event arrive twice?
Yes. A retry resends an event your endpoint may already have processed, for example when it answered after the 8 second timeout. On X, one post can also arrive as several events that share an id and differ in type. To de-duplicate, key on X-1322-Event-Id and X-1322-Event-Type together.
How do I get a new signing secret?
The secret is shown once, when you add the destination. To replace it, delete the destination in Settings > Webhooks and add it again; the new destination gets a new secret.
My endpoint receives nothing. What should I check?
Open the delivery log in Settings > Webhooks. Check that the destination is enabled, that you track accounts on its source, that the endpoint answers 2xx within 8 seconds without redirecting, and that its hostname resolves only to public addresses.
Related
- API reference: REST endpoints and WebSocket event shapes for every source.
- WebSocket quick starts: one page per source, with Python and Node consumers.
- Docs MCP server: the same reference for MCP clients.