> For the complete documentation index, see [llms.txt](https://docs.fuul.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fuul.xyz/developer-guide/terms-conditions.md).

# Terms & Conditions

Projects can require users to accept a terms document before participating in a program. Fuul records the acceptance once per wallet and makes it readable from any surface holding a project API key, so a user who already accepted in your app is not prompted again in the affiliate portal.

{% hint style="info" %}
This feature is configured by Fuul, not from the dashboard. Reach out at <ecosystem@fuul.xyz> to enable it for your project and set your terms document URL. Until it is enabled, `GET /status` reports `terms_acceptance_required: false` and `POST /accept` returns `400`.
{% endhint %}

## How it works

1. Your frontend calls `GET /status` for the connected wallet
2. If `terms_acceptance_required` is `true` and `terms_accepted` is `false`, you show the document at `terms_document_url` and ask the user to accept
3. Your backend calls `POST /accept`, declaring which surface collected the acceptance
4. Every other surface reading `/status` for that wallet now sees `terms_accepted: true`

The record attests that a system asserted the acceptance. Neither endpoint carries a user session, and nothing cryptographically binds the wallet to the request, so this is not a signature scheme. If you need proof of possession, verify the wallet yourself before calling `/accept`.

## Endpoints

Base path: `/api/v1/project-terms-conditions`

| Endpoint  | Method | Required scope                             | Description                                                     |
| --------- | ------ | ------------------------------------------ | --------------------------------------------------------------- |
| `/accept` | POST   | `terms_conditions:write`                   | Records an acceptance. Returns `201`                            |
| `/status` | GET    | `terms_conditions:write` or `service_role` | Returns the project's config plus one wallet's acceptance state |

Both endpoints return the same response shape, so an accept never needs a follow-up read.

### Request fields

| Field                  | Where       | Description                                                           |
| ---------------------- | ----------- | --------------------------------------------------------------------- |
| `user_identifier`      | Both        | The wallet address                                                    |
| `user_identifier_type` | Both        | `evm_address`, `solana_address`, or `xrpl_address`                    |
| `source`               | Accept only | `partner_site`, `affiliates_portal`, or `other`. Required, no default |

{% hint style="warning" %}
The accepted identifier types are **narrower than the platform's full list**. Only `evm_address`, `solana_address`, and `xrpl_address` are valid here. `sui_address` is rejected with a `400` even though it appears in identifier type lists elsewhere in these docs, and `email` / `uuid` are rejected as well: an acceptance is anchored to a wallet.
{% endhint %}

### Response

```json
{
  "terms_acceptance_required": true,
  "terms_document_url": "https://yourproject.com/terms-v2.pdf",
  "terms_accepted": true,
  "accepted_at": "2026-07-23T13:52:18.000Z"
}
```

| Field                       | Description                                                                   |
| --------------------------- | ----------------------------------------------------------------------------- |
| `terms_acceptance_required` | Whether the gate is on for this project                                       |
| `terms_document_url`        | The document currently in force. Only present when the gate is on             |
| `terms_accepted`            | Whether this wallet has already accepted                                      |
| `accepted_at`               | When the acceptance was recorded, on the server clock. `null` if not accepted |

## Example

Check status before showing your program UI:

```typescript
const res = await fetch(
  `https://api.fuul.xyz/api/v1/project-terms-conditions/status?user_identifier=${address}&user_identifier_type=evm_address`,
  { headers: { Authorization: `Bearer ${apiKey}` } }
);

const status = await res.json();

if (status.terms_acceptance_required && !status.terms_accepted) {
  showTermsModal(status.terms_document_url);
}
```

Record the acceptance from your backend:

```bash
curl -X POST https://api.fuul.xyz/api/v1/project-terms-conditions/accept \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-api-key" \
  -d '{
    "user_identifier": "0x1234...",
    "user_identifier_type": "evm_address",
    "source": "partner_site"
  }'
```

## Idempotency

Acceptance is unique per `(project, wallet, identifier type)`. Calling `/accept` again for a wallet that already accepted returns `201` and leaves the original record untouched: `source`, the document URL, and `accepted_at` stay frozen to whichever surface accepted first.

This means you can call `/accept` without checking `/status` first, and retries are safe.

## What acceptance does and does not cover

|                          | Behavior                                                                                                                                                        |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Scope**                | One general acceptance per wallet, not per document revision. There is no version field                                                                         |
| **Document changes**     | Each record snapshots the document URL that was live when the user accepted. Publish every revision under a new immutable URL so that snapshot stays meaningful |
| **Re-prompting**         | Changing your document text does **not** re-prompt users who already accepted                                                                                   |
| **Turning the gate off** | Existing records are kept and become inert, not deleted. Turning the gate back on does not re-prompt anyone who already accepted                                |

{% hint style="warning" %}
If you overwrite your terms document at the same URL instead of publishing a new one, the stored records will point at a document whose contents have changed since users accepted it. Use a versioned path such as `/terms-v2.pdf`.
{% endhint %}

## Related

* Collecting tax forms from affiliates is a separate flow with its own gate → [Tax Information](/core-concepts/affiliates/tax-information.md)
* Programs that require users to accept individual payouts before claiming → [Claim Flow Integration](/developer-guide/claim-frontend.md)
