Webhooks
When a rule matches, Farbeam sends an HTTP POST with a JSON body to the rule URL. This page defines the request, what your endpoint must return, and the limits.
#The request
POST /hooks/farbeam HTTP/1.1
Host: example.com
content-type: application/json
content-length: <body length>
user-agent: farbeam-webhooks/1
x-farbeam-signature: t=1793821030,v1=<64 hex characters>
x-farbeam-rule: rule_c3208c320585| Header | Value |
|---|---|
content-type | application/json |
user-agent | farbeam-webhooks/1 |
x-farbeam-signature | t=<unix seconds>,v1=<hex>. See webhook signatures. |
x-farbeam-rule | The rule id |
The URL path and query are sent as you saved them. A query string token in your URL is a way to add routing data, though the signature is what proves the sender.
#Payload
There are three types: transfer.matched, liquidity.matched and rule.test.
| Field | Type | Meaning |
|---|---|---|
type | string | transfer.matched, liquidity.matched or rule.test |
rule_id | string | Rule id |
rule_name | string | Rule name when the delivery was queued |
sent_at | string | UTC time the delivery was queued. It stays the same across retries. |
data | object or null | The transfer or the liquidity sample, or null on rule.test |
#transfer.matched
The data object uses the fields of the transfer object. The row in this sample is real, stored on 2026-11-04 UTC. The envelope was built with the service's own formatter, with an example rule.
{
"type": "transfer.matched",
"rule_id": "rule_c3208c320585",
"rule_name": "Large moves",
"sent_at": "2026-11-04T19:37:10.000Z",
"data": {
"uid": "ethereum:0xcc9606e4d3febb135994e9f4035de34c791f2c0cd58bee75c8a7df40e488841a:210",
"ts": 1793821007000,
"chain": "ethereum",
"block": 26343583,
"tx": "0xcc9606e4d3febb135994e9f4035de34c791f2c0cd58bee75c8a7df40e488841a",
"kind": "token",
"token": "WETH",
"token_address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"amount": 743.5137099679833,
"usd": 1800098.7377921855,
"from": "0x76f30e3f75437fb862b8d2c4d80a671bceba5b1a",
"to": "0x000000000035b5e5ad9019092c665357240f594e",
"sampled": false,
"id": 3485,
"from_label": null,
"from_type": null,
"to_label": null,
"to_type": null
}
}#liquidity.matched
A rule that names liquidity_usd or change_pct sends this type. See Beamline fields. This sample is an example built from the service's event shape. It is not a real reading.
{
"type": "liquidity.matched",
"rule_id": "rule_000000000000",
"rule_name": "Liquidity drop",
"sent_at": "2026-11-04T12:00:03.000Z",
"data": {
"uid": "liq:solana:JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN:1793880000000",
"ts": 1793880000000,
"kind": "liquidity",
"chain": "solana",
"token": "JUP",
"token_address": "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
"liquidity_usd": 1000000,
"change_pct": -31.5,
"window_minutes": 55
}
}#rule.test
{
"type": "rule.test",
"rule_id": "rule_c3208c320585",
"rule_name": "Large stablecoin moves",
"sent_at": "2026-11-04T19:37:09.496Z",
"data": null
}Handle data: null in your code, or a test delivery will crash your receiver.
#What your endpoint must do
| Requirement | Detail |
|---|---|
| Answer with 2xx | Any code from 200 to 299 counts as delivered. The body is ignored. |
| Answer within 6 seconds | Longer is a timeout failure |
| Verify the signature | On the raw bytes, before you parse the JSON |
| Remove duplicates | Use data.uid. See below. |
| Do slow work later | Put the event on your own queue, then answer |
A redirect (3xx) counts as a failure, because redirects are not followed. Use the final URL.
#Duplicates
A delivery can arrive more than once. Your endpoint may finish its work and fail to answer in time, and the retry then repeats it. There is no delivery id in the payload, so use data.uid plus rule_id as your key.
#Ordering
Deliveries are not ordered. A retry can arrive after newer events. Use data.block or data.ts if order matters.
#URL rules
These rules run when you create or change a rule, with the error code invalid_url.
| Rule | Message |
|---|---|
| Must parse as a URL | url is not a valid URL |
| Must use HTTPS | url must use https |
| No user name or password in the URL | url must not contain credentials |
| Port must be empty or 443 | url must use port 443 |
| At most 500 characters | url is too long |
Must be a host name. No IP addresses. No localhost, *.local or *.internal. | url must use a public host name |
#SSRF protection
The service resolves your host name again before each send. All returned addresses must be public, and the connection uses the address that was checked, so a change in DNS between check and connect cannot redirect it. If any address is private, the attempt fails with host resolves to a private address.
These ranges are refused.
| Family | Ranges |
|---|---|
| IPv4 | private ranges (RFC 1918), loopback, link-local, carrier-grade NAT, benchmark ranges, multicast and reserved ranges |
| IPv6 | ::, ::1, fc00::/7, fe80::/10, ff00::/8, and IPv4-mapped forms of the IPv4 ranges |
Host names are checked at create time for form only. DNS is checked at send time, so a rule can save and then fail its first delivery with this error.
#Limits
| Limit | Value | When it is reached |
|---|---|---|
| Cooldown | 60 to 86,400 s, default 300 | After a rule fires, matches are dropped until it ends. No log row is written, and they are not sent later. |
| Deliveries per rule per hour | 120 | Further matches in that UTC hour are dropped. No log row is written, and they are not sent later. |
| Rules per account | 10 | POST /rules returns 409 rule_limit. Disabled rules count. |
| Test deliveries per rule | 10 per hour | 429 rate_limited |
| Failures in a row before auto-off | 20 deliveries | The rule turns off. See below. |
| Send timeout | 6 seconds per attempt | Counts as a failed attempt |
| Log retention | 14 days | Rows are deleted |
#The cooldown and the hourly cap
After a rule fires, it waits for cooldown_seconds before it can fire again. Read last_fired_at on the rule to see the last time. Set cooldown_seconds when you create or update a rule. Because the shortest cooldown is 60 s, matches alone reach at most 60 per hour, and the cooldown acts before the cap.
The count resets at the start of each UTC hour. Test deliveries add to the count, though the cap does not block them. A retry does not add to it. If a rule reaches 120, you lose the rest of the matches for that hour with no notice in the log. If you need a count of what you missed, compare against List large transfers with the same filters. If you hit the cap often, raise the usd floor.
#Auto-off
A delivery has failed when all four attempts have failed. Twenty such deliveries in a row turn the rule off, and enabled reads false with consecutive_failures at 20. One success resets the count to 0.
No notice is sent when this happens. Monitor it with List rules. To turn the rule back on, fix your endpoint, then send PATCH /rules/{id} with {"enabled":true}. This also clears the count. Matches during the off period are not sent later.
#Delivery log
List deliveries returns recent rows for a rule.
{
"id": 1,
"ts": "2026-11-04T19:22:56.834Z",
"transfer_uid": "test",
"status": "pending",
"attempts": 1,
"code": null,
"error": "connection failed"
}| Field | Values |
|---|---|
status | pending, delivered or failed |
attempts | Attempts so far, up to 4 |
code | Your last HTTP status, or null |
error | non-2xx reply, timeout, connection failed, a URL safety message, or null |
transfer_uid | The row uid, or test |
The payload itself is cleared once a delivery ends, either way. See privacy and data.
#More
- Signature verification in four languages.
- Retry schedule.
- Discord format.
- How to test.