> 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-reference/verification.md).

# Verification

## `POST /v1/quests/verify`

Checks whether a linked user has completed an action. A user token needs the `quests:verify` scope. An app token must name a `user_id` who granted it. Answers come from live data, so if a user unlikes a post, `verified` becomes `false` again.

A completed check and an incomplete one both return `200`. Read the `verified` field rather than the status code. Errors such as an unlinked user or a missing target use [other status codes](/developer-resources/errors.md).

### Request

| Field            | Required                  | Notes                                                                                                      |
| ---------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `action`         | Yes                       | `like_post`, `comment_post`, `repost_post`, `quote_post`, `follow_user`, `join_community` or `create_post` |
| `target_id`      | Yes, except `create_post` | See below                                                                                                  |
| `user_id`        | App tokens only           | `usr_<id>` or `<id>`, not `@username`. With a user token, it must match that user or be left out           |
| `campaign_id`    | Strongly recommended      | Up to 128 letters, digits and `: _ . -`. Gives each campaign its own receipt                               |
| `campaign_start` | No                        | RFC 3339 with a timezone, such as `2026-11-03T00:00:00Z`. Earlier actions don't count                      |
| `campaign_end`   | No                        | RFC 3339 with a timezone. Later actions don't count. It can't be before `campaign_start`                   |
| `rules`          | `create_post` only        | See [Rules](#rules)                                                                                        |

{% hint style="warning" %}
Always send `campaign_id`. Without it, every check for the same user, action and target shares one receipt, so a second campaign on the same post returns `already_verified: true` and could look like a duplicate claim.
{% endhint %}

| Action           | `target_id` forms                 | Counts when                                                                                                                           |
| ---------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `like_post`      | `post_<id>` or `<id>`             | The user currently likes the post. Emoji reactions don't count                                                                        |
| `comment_post`   | `post_<id>` or `<id>`             | The user has a live direct reply to the post. `completed_at` is the earliest. Quotes don't count                                      |
| `repost_post`    | `post_<id>` or `<id>`             | The user has a live, public repost of the post. Quotes don't count                                                                    |
| `quote_post`     | `post_<id>` or `<id>`             | The user has a live, public quote of the post. `completed_at` is the earliest. Plain reposts don't count                              |
| `follow_user`    | `usr_<id>`, `<id>` or `@username` | The user follows the account. A pending request doesn't count                                                                         |
| `join_community` | `com_<id>`, `<id>` or `@handle`   | The user is a member, and any paid membership hasn't lapsed. Members of token-gated communities who are in a grace period still count |

Post targets must be public. A followers-only, paid, gated or private-account post returns `404 target_not_found`, the same as a post that doesn't exist.

### Example request with an app token

```bash
curl https://api.floral.fun/v1/quests/verify \
  -H "Authorization: Bearer $APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "usr_123",
    "action": "follow_user",
    "target_id": "@floral",
    "campaign_id": "spring-launch",
    "campaign_start": "2026-11-03T00:00:00Z",
    "campaign_end": "2026-11-10T00:00:00Z"
  }'
```

### Response

A completed action:

```json
{
  "verified": true,
  "reason": null,
  "action": "follow_user",
  "user_id": "usr_123",
  "target_id": "usr_789",
  "campaign_id": "spring-launch",
  "completed_at": "2026-11-04T12:00:00.000Z",
  "verification_id": "ver_8f14e45f",
  "first_verified_at": "2026-11-04T12:05:00.000Z",
  "already_verified": false
}
```

An action done before the campaign started:

```json
{
  "verified": false,
  "reason": "before_campaign",
  "action": "follow_user",
  "user_id": "usr_123",
  "target_id": "usr_789",
  "campaign_id": "spring-launch",
  "completed_at": "2026-10-01T08:00:00.000Z",
  "verification_id": null,
  "first_verified_at": null,
  "already_verified": false
}
```

`target_id` always comes back in its canonical form, even if you sent a `@handle`.

Fields:

| Field               | Notes                                                                                                                                                                                                                |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verified`          | Whether the action is done and falls inside the campaign window                                                                                                                                                      |
| `reason`            | Null when verified. Otherwise `not_completed`, `not_public`, `before_campaign`, `after_campaign` or `completion_time_unknown`, which means you set a campaign window and Floral has no time on record for the action |
| `completed_at`      | When the user actually did it, or null                                                                                                                                                                               |
| `verification_id`   | Stable receipt for this partner, user, action, target and campaign. Null if not verified                                                                                                                             |
| `first_verified_at` | When this receipt was first issued                                                                                                                                                                                   |
| `already_verified`  | `true` when the receipt already existed                                                                                                                                                                              |

Actions exactly on `campaign_start` or `campaign_end` count.

### Errors

| Status | Code                                                     | When                                                            |
| ------ | -------------------------------------------------------- | --------------------------------------------------------------- |
| 400    | `invalid_action`                                         | `action` isn't one of the values above                          |
| 400    | `invalid_target`                                         | `target_id` is missing or malformed, or sent with `create_post` |
| 400    | `invalid_user_id`, `user_id_required`                    | An app token sent a malformed `user_id`, or none                |
| 400    | `invalid_campaign_id`, `invalid_campaign_window`         | A campaign field is malformed, or `create_post` is missing one  |
| 400    | `invalid_rules`                                          | A rule is invalid, or `rules` was sent with another action      |
| 403    | `insufficient_scope`, `user_mismatch`, `user_not_linked` | The token can't ask about this user                             |
| 404    | `target_not_found`                                       | The target doesn't exist or isn't public                        |

### Preventing duplicate rewards

Award a reward only when `verified` is `true` and `already_verified` is `false`, or key your reward ledger on `verification_id`. Concurrent checks produce one receipt, and exactly one response reports `already_verified: false`.

A receipt remains after an action is undone, so a user who likes, gets checked, unlikes and likes again keeps the same `verification_id`.

Floral records when the current like or follow happened, so undoing either and doing it again moves `completed_at` to the new time. The same applies to a repost or quote that's removed and made again. Someone who already followed an account can unfollow and follow again to fall inside a campaign window. If a campaign is meant for new followers only, this API can't tell them apart from returning ones, so plan for that in your rules.

Community membership works differently. Rejoining normally keeps the original join date, so a former member who rejoins during a campaign usually returns `before_campaign`.

`not_public` means the user did the action, but a repost or quote that's limited to followers, friends or close friends, set to only me, or posted from a private account doesn't count. Ask them to make it public or use a public account.

Check when a participant asks, rather than polling. See [Rate limits](/developer-resources/rate-limits.md).

## Verify a new post with `create_post`

`create_post` asks whether the user published a post that matches your campaign's rules, so it takes `rules` instead of `target_id`. `campaign_id` and `campaign_start` are required, so a post from before the campaign can't count.

```bash
curl https://api.floral.fun/v1/quests/verify \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create_post",
    "campaign_id": "spring-launch",
    "campaign_start": "2026-11-03T00:00:00Z",
    "campaign_end": "2026-11-10T00:00:00Z",
    "rules": {
      "hashtags_any": ["floral"],
      "mentions_any": ["@floral"],
      "min_text_length": 20
    }
  }'
```

```json
{
  "verified": true,
  "reason": null,
  "action": "create_post",
  "user_id": "usr_123",
  "target_id": null,
  "matched_post_id": "post_456",
  "campaign_id": "spring-launch",
  "completed_at": "2026-11-04T12:00:00.000Z",
  "verification_id": "ver_8f14e45f",
  "first_verified_at": "2026-11-04T12:05:00.000Z",
  "already_verified": false
}
```

### Rules

All rules are optional. With none, any public original post in the window counts. A post must meet every rule you send.

| Rule              | Default        | Meaning                                                                                           |
| ----------------- | -------------- | ------------------------------------------------------------------------------------------------- |
| `post_types`      | `["original"]` | Any of `original`, `quote` and `reply`. Plain reposts never count                                 |
| `hashtags_any`    | None           | Has at least one. Up to 10, with or without `#`, not case-sensitive. Letters, digits and `_` only |
| `hashtags_all`    | None           | Has all of them                                                                                   |
| `mentions_any`    | None           | Mentions or tags at least one. Up to 10, as `@handle` or `usr_<id>`                               |
| `mentions_all`    | None           | Mentions or tags all of them                                                                      |
| `min_text_length` | `0`            | Minimum characters of writing, up to 5000. Embedded cards don't count                             |
| `require_media`   | `false`        | Must have a photo or video                                                                        |

Unknown rules return `400 invalid_rules`, so a typo can't widen a campaign. So does a mention of an account that doesn't exist.

A post's topics count as its hashtags, because the Floral composer can turn a hashtag into a topic.

### How matching works

* **Public posts only.** A non-public post returns `not_completed`, and `reason` never says one exists. Users on private accounts can't complete this action.
* **Earliest match.** `matched_post_id` and `completed_at` describe the earliest match among the user's first 200 public posts in the window.
* **Live results.** If the post is deleted, or edited so it no longer matches, `verified` becomes `false`. The receipt remains.
* **One receipt** per user, campaign and set of rules. Rule order and letter case don't matter, but changing a rule creates a new receipt.


---

# 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-reference/verification.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.
