> For the complete documentation index, see [llms.txt](https://docs.floral.fun/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.floral.fun/api-v2/v2-webhooks-and-bulk.md).

# Webhooks and bulk ingestion

Webhooks push events to you as they happen, and bulk reading lets you catch up from a position in the stream. Both carry the same events, and both need approval from Floral: email <main@floral.fun> with your application's client ID and what you'll use them for.

## Events

| Type               | `data`                        | Sent when                                                     |
| ------------------ | ----------------------------- | ------------------------------------------------------------- |
| `post.created`     | `post`                        | A public post is published                                    |
| `reply.created`    | `post`                        | A public reply is published, under a public thread            |
| `quote.created`    | `post`                        | A public quote is published                                   |
| `repost.created`   | `post`                        | A public repost is made, of a public post                     |
| `post.unavailable` | `post_id`                     | A post is deleted, or stops being public                      |
| `follow.created`   | `follower_id`, `following_id` | Someone follows someone, and both people's lists are public   |
| `follow.removed`   | `follower_id`, `following_id` | Someone unfollows someone, and both people's lists are public |
| `user.restricted`  | `user_id`                     | An account's visibility decreases, or it's deleted            |

Every event looks like this:

```json
{
  "id": "6f1c2a90-3b1e-4c55-9d0a-2f7b8e4a1c33",
  "type": "post.created",
  "schema_version": 1,
  "occurred_at": "2026-11-04T12:00:00.000Z",
  "data": { "post": { "id": "post_456", "text": "gm" } }
}
```

The `post` above is shortened, and it has the full [V2 post fields](/api-v2/v2-overview.md#post-fields). Use `id` to ignore duplicates.

Events are checked at the moment they're sent, so some are never sent at all. See [Privacy and visibility](/api-v2/v2-privacy.md). A new `schema_version` means the format changed, so check it before you process an event.

## Webhooks

### Register

In the [Developer Portal](https://floral.fun/developers), open your application and add a webhook:

* **URL:** public and https. Addresses on private networks are refused, and redirects aren't followed
* **Events:** the types you want
* **Filters:** optional lists of authors and communities, up to 100 each

An application can have up to 5 webhooks. The **signing secret** is shown once, so copy it straight away. Use **Send test** to send a signed `ping` to your receiver.

### Verify every request

Each delivery has these headers:

| Header                 | Meaning                                   |
| ---------------------- | ----------------------------------------- |
| `X-Floral-Signature`   | `t=<unix seconds>,v1=<signature>`         |
| `X-Floral-Event-Id`    | The event's `id`, the same on every retry |
| `X-Floral-Delivery-Id` | This event's delivery to this webhook     |

The signature is an HMAC-SHA256 of `<t>.<raw body>`, using your signing secret. Check it against the raw body before parsing, and refuse anything older than 5 minutes so a captured request can't be replayed:

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyFloralWebhook(rawBody, header, secret) {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
  if (!match) return false;
  const [, timestamp, signature] = match;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
```

### Respond and retry

Respond with any `2xx` within 8 seconds, and do the work afterwards. Anything else is retried after 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours and 24 hours, and then marked as failed. Each retry is checked for privacy again, and may not be sent.

Delivery order isn't guaranteed, so use `occurred_at`. After 100 failures in a row the webhook pauses, and you can resume it in the Developer Portal. Delivery history is kept for 30 days.

## Bulk reading

`GET /v2/bulk/events` returns the same events, oldest first:

| Parameter | Notes                                                              |
| --------- | ------------------------------------------------------------------ |
| `cursor`  | From the previous response. Leave it out to start at the beginning |
| `limit`   | 1 to 1000, default 500                                             |
| `types`   | Comma-separated event types                                        |

```bash
curl "https://api.floral.fun/v2/bulk/events?types=post.created,post.unavailable&limit=1000" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{ "data": [], "next_cursor": "c2VxOjEyMzQ1", "has_more": true }
```

Loop until `has_more` is `false`, then store `next_cursor` and resume from it later. **A page can be empty with more to come**, because events that can't be shown are skipped. The newest 15 seconds aren't included yet, because they may still be arriving out of order.

Read `post.unavailable` and `user.restricted` too, and delete what they tell you to.

History starts when Floral began recording, so older activity may be missing, and old data can be removed under Floral's retention policy.

For the current state of something, read it directly. The stream says what happened, and only the regular endpoints say what's true now.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.floral.fun/api-v2/v2-webhooks-and-bulk.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
