Skip to main content
API reference

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.

All 8 sources · HTTPS only · 8 second timeout · up to 6 attempts
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 POST with 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

  1. Open Settings > Webhooks in the dashboard.
  2. 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.
  3. Copy the signing secret: whsec_ followed by 64 hex characters. It is shown once. After that the dashboard only shows a masked form.
  4. Press Test to send one signed test.ping event. A test send is a single attempt with no retries, and it does not count toward auto-disable.
  5. 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:

HeaderValue
Content-Typeapplication/json
X-1322-Event-IdThe event id. For live events it equals id in the body. Retries keep the same value.
X-1322-Event-TypeThe event type. For live events it equals type in the body; a test send uses test.ping.
X-1322-TimestampUnix time in milliseconds when this attempt was signed, for example 1794312000000. New on every attempt.
X-1322-Signaturesha256= 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
  1. Read the raw request body bytes before any JSON parsing. A body that was parsed and serialized again will not match.
  2. Build the message: the X-1322-Timestamp value exactly as sent (milliseconds, digits only), a ., then the raw body.
  3. Compute HMAC-SHA256 with your whole secret as the key, whsec_ prefix included, encode it as lowercase hex and put sha256= in front.
  4. Compare the result with X-1322-Signature using a constant-time comparison, and reject the request if they differ.
  5. 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=3b67daeb880cdb79200c71f459f17c3475dd5a453bcee8ea15e39d200678fa17

Verify 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 so req.body is the raw Buffer, and pass that to verify1322.
  • Keep the secret on your server, in an environment variable or a secret store.

Payload

The body is one JSON object:

FieldTypeMeaning
idstringThe event id, for example x:tweet:1900000000000000001.
platformstringx, truth, instagram, youtube, tiktok, binance, news or fomo.
typestringThe event type for that source.
sent_atstringISO 8601 time the body was built. Retries resend the same body, so it does not change between attempts.
eventobjectThe 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.

FieldsMeaning
id, platform, eventTypeThe same values as id, platform and type in the envelope.
account, handle, avatar, userIdWho posted: display name (the feed name for News), handle without @, avatar URL, the platform's user id.
followers, following, verifiedAccount counts and the verification badge, a boolean or an object with type and label.
content, contentType, title, sourceUrlThe 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.
mediaArray of { kind, url } with kind image, video, gifv, audio or other, plus posterUrl, caption, width, height and durationS when known.
quotedPost, replyTo, isRepost, repostOfThe quoted post, the post replied to, and repost details.
metricsEngagement counts when the source has them: likes, retweets, replies, quotes, views, comments, shares.
timestampThe event time reported by the source.
fomoFOMO only: kind (thesis, trade, token_verified and so on) with coin, trade, position and thesis details.
rawThe 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 id and a different type, 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-Id and X-1322-Event-Type, and a new X-1322-Timestamp and X-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 timestamp when 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, .internal or .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