> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omneo.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Invites API reference

> Every endpoint of the Omneo Invites service, the invite object, status lifecycle, and error responses.

The Invites API is the HTTP interface of [Omneo Invites](/experiences/invites/overview). It is a separate service from the Omneo core API and is not part of the [API Reference](/api-reference/introduction) OpenAPI specification. This page documents it by hand. For the recommended integration path, read [Integrating Omneo Invites](/dev-guides/frontend/invites-plugin) first.

## Base URL, authentication, and limits

Production base URL:

```text theme={null}
https://api.[tenant].getomneo.com/invites/
```

Omneo confirms the staging equivalent. Paths below are relative to this base.

| Route group                   | Authentication                                                                                                                            | Who calls it                                                      |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `/api/v1/me/...`              | `Authorization: Bearer {Omneo ID token}`, a short-lived token scoped to one profile. See [Using Omneo ID](/dev-guides/frontend/omneo-id). | Your backend, acting as the referrer.                             |
| `/api/v1/invites/...`         | `Authorization: Bearer {machine token}`, a long-lived server credential issued by Omneo.                                                  | Profile Portal, or your backend if your site performs the accept. |
| `/api/health`, `/api/version` | None                                                                                                                                      | Monitoring                                                        |

All responses are JSON regardless of the `Accept` header. Every endpoint is rate-limited to 60 requests per minute per token, or per IP address when unauthenticated. An invalid ID token returns `401` with an empty message. An invalid machine token returns `401` with `{"message":"Unauthenticated."}`.

## Endpoints

| Method | Path                             | Auth          | Purpose                                              |
| ------ | -------------------------------- | ------------- | ---------------------------------------------------- |
| `POST` | `/api/v1/me/invites`             | ID token      | Create an invite and send the email                  |
| `GET`  | `/api/v1/me/invites`             | ID token      | List the referrer's invites with quota metadata      |
| `GET`  | `/api/v1/me/invites/{id}`        | ID token      | Read one of the referrer's invites                   |
| `POST` | `/api/v1/me/invites/{id}/resend` | ID token      | Resend the invitation email                          |
| `POST` | `/api/v1/me/invites/{id}/revoke` | ID token      | Cancel a pending invite                              |
| `POST` | `/api/v1/invites/{id}/validate`  | Machine token | Check an invite can be accepted for an email address |
| `POST` | `/api/v1/invites/{id}/accept`    | Machine token | Accept an invite on the invitee's behalf             |
| `GET`  | `/api/v1/invites/{id}`           | Machine token | Read any invite by ID                                |
| `GET`  | `/api/health`                    | None          | Returns `200` with an empty body                     |
| `GET`  | `/api/version`                   | None          | Returns the service version as plain text            |

<Warning>
  `POST /api/v1/invites` (without `/me/`) also exists. It supports a legacy integration and does not send the email, enforce quota, or honour opt-outs. Do not build against it.
</Warning>

### Create an invite

`POST /api/v1/me/invites`

| Field                  | Type   | Rule                                       |
| ---------------------- | ------ | ------------------------------------------ |
| `recipient_first_name` | string | Required. The first letter is capitalised. |
| `recipient_email`      | string | Required. Must be a valid email address.   |
| `public_message`       | string | Optional. Passed to the email template.    |

Before creating, the service resolves the referrer's Invite Definition from their profile status and checks eligibility: the definition is within its campaign window, and the referrer has invites remaining or an override. If the address has opted out, the request is refused. If the address has already accepted an invitation, the service returns a normal-looking pending invite and sends no email, so the response never reveals whether an address is already in the program.

Returns `200` with the [invite object](#the-invite-object) in `data`. The invitation email is sent during the request.

| Status | Body                                                                                                                                           |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `{"data":{"error":"Refusing to create invitation as the eligibility check failed (not enough invites remaining or definition not in range)"}}` |
| `403`  | `{"data":{"error":"This action is forbidden."}}` when the address or its domain has opted out                                                  |
| `422`  | `{"message":"The given data was invalid.","errors":{...}}`                                                                                     |

### List invites

`GET /api/v1/me/invites`

Returns every invite the referrer has sent, ordered oldest first by `invited_at`, plus a `meta` object:

```json theme={null}
{
  "data": [ { "...": "..." } ],
  "meta": {
    "remaining": 3,
    "override_count": null,
    "profile_statuses": ["member"],
    "eligible_definition": {
      "id": 1,
      "handle": "refer-a-friend",
      "in_range": true,
      "starts_at": null,
      "ends_at": null,
      "max_invites": 5
    }
  }
}
```

| Field                      | Meaning                                                                                            |
| -------------------------- | -------------------------------------------------------------------------------------------------- |
| `meta.remaining`           | Invites the referrer can still send. `null` means unlimited.                                       |
| `meta.override_count`      | A per-profile limit set by Omneo that replaces `max_invites`, or `null`.                           |
| `meta.profile_statuses`    | The referrer's current profile statuses.                                                           |
| `meta.eligible_definition` | The Invite Definition that applies to this referrer, with its campaign window and `in_range` flag. |

### Read one invite

`GET /api/v1/me/invites/{id}`

Returns `200` with the invite object, or `404` if the invite does not exist or belongs to a different referrer.

### Resend an invite

`POST /api/v1/me/invites/{id}/resend`

Returns `202` with `{"data":{"message":"Invite scheduled for re-send."}}`, or `400` with `{"data":{"error":"Invite expired."}}`. Resends are capped by a tenant setting, three by default. Beyond the cap the call still returns `202` and no email is sent. Resends are also suppressed for invites that are not pending or whose address has opted out.

### Revoke an invite

`POST /api/v1/me/invites/{id}/revoke`

Returns `200` with the invite now `REVOKED`, or `400` with `{"data":{"error":"Cannot revoke a non-pending invite."}}`. Revoked invites do not count against quota.

### Validate an invite

`POST /api/v1/invites/{id}/validate`

| Field             | Type   | Rule                                              |
| ----------------- | ------ | ------------------------------------------------- |
| `recipient_email` | string | Required. Must equal the invited address exactly. |

Returns `200` with an empty body when the invite is pending, unexpired, and the address matches. Otherwise returns `422` with `{"data":{"error":"Validation failed as incoming email is different to invite email."}}`, or one of the [state errors](#error-responses).

### Accept an invite

`POST /api/v1/invites/{id}/accept`

| Field                   | Type    | Rule                                                                                                                             |
| ----------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `recipient_email`       | string  | Required. Must equal the invited address exactly.                                                                                |
| `recipient_external_id` | string  | Required. The invitee's ID on your platform. Linked to the profile as an identity under the tenant's configured identity handle. |
| `first_name`            | string  | Required.                                                                                                                        |
| `last_name`             | string  | Required.                                                                                                                        |
| `email_promo`           | boolean | Optional. Accepted but not used.                                                                                                 |

The service checks the invite state before validating the body, so a malformed request against an expired invite returns `410`, not `422`. On success it finds the profile by `recipient_external_id`, then by email, or creates one; writes the custom attribute `invites.invited_by` with the referrer's profile ID; applies the definition's default statuses; marks the invite `ACCEPTED`; and marks any other pending invites to the same address `DECLINED`.

Returns `200` with the profile, not the invite:

```json theme={null}
{ "profile_id": "01H9A...", "statuses": ["member"] }
```

Returns `422` with `{"data":{"error":"Refusing to accept as incoming email is different to invite email."}}` on an address mismatch, or one of the [state errors](#error-responses).

### Read any invite

`GET /api/v1/invites/{id}`

Returns `200` with the invite object for any referrer, or `404`.

## The invite object

| Field                                      | Type            | Notes                                                                                                                                         |
| ------------------------------------------ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                                       | UUID string     | Use wherever a path says `{id}`.                                                                                                              |
| `source_omneo_id`                          | string          | The referrer's Omneo profile ID.                                                                                                              |
| `recipient_email`                          | string          | The invited address.                                                                                                                          |
| `status`                                   | string          | One of `PENDING`, `ACCEPTED`, `REVOKED`, `DECLINED`, `EXPIRED`.                                                                               |
| `should_communicate`                       | boolean or null | Internal. Ignore.                                                                                                                             |
| `send_count`                               | integer         | Emails sent so far. The create response is built before the email goes out, so it may read `0` there. The list endpoint shows the real count. |
| `invited_at`                               | string          | `YYYY-MM-DD HH:MM:SS` in the service's local time, not ISO 8601 or UTC.                                                                       |
| `expires_at`                               | string or null  | Same format. End of day, the definition's period in days after `invited_at`. `null` means never expires.                                      |
| `accepted_at`, `declined_at`, `revoked_at` | string or null  | Same format.                                                                                                                                  |
| `definition`                               | string          | Handle of the Invite Definition, for example `refer-a-friend`.                                                                                |
| `meta`                                     | object or null  | Internal. Ignore.                                                                                                                             |

The response does not include `recipient_first_name` or `public_message`.

## Status lifecycle

| From      | To         | Cause                                                                                                 |
| --------- | ---------- | ----------------------------------------------------------------------------------------------------- |
| `PENDING` | `ACCEPTED` | The invitee joined and the accept succeeded.                                                          |
| `PENDING` | `REVOKED`  | The referrer revoked it.                                                                              |
| `PENDING` | `DECLINED` | The invitee declined or opted out, or a different referrer's invite to the same address was accepted. |
| `PENDING` | `EXPIRED`  | The invite passed `expires_at`.                                                                       |

Invites past `expires_at` are refused at validate and accept with `410`, but their `status` field is not automatically changed to `EXPIRED`. When displaying a referrer's invites, treat `status` equal to `PENDING` with `expires_at` in the past as expired.

## Error responses

Three envelopes appear. Check the status code first, then the body.

```json theme={null}
{ "data": { "error": "..." } }
```

Service rules: `400`, `403`, `409`, `410`, and the `422` email mismatch.

```json theme={null}
{ "message": "The given data was invalid.", "errors": { "field": ["..."] } }
```

Validation, `422`.

```json theme={null}
{ "message": "Unauthenticated." }
```

`401` on machine-token routes. ID-token routes return `401` with an empty message.

State errors from validate and accept:

| Status | Body                                                                                                                                         |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `{"data":{"error":"The invite is not in a pending state."}}`                                                                                 |
| `409`  | `{"data":{"error":"The invite has already been accepted."}}` The address has already accepted an invitation, possibly from another referrer. |
| `410`  | `{"data":{"error":"The invite has expired."}}`                                                                                               |
| `404`  | Unknown invite ID, or on `/me/` routes an invite that belongs to a different referrer.                                                       |

## Behaviour to design around

* **Duplicates are not rejected.** Inviting the same address twice creates two pending invites, and both count against quota. De-duplicate against the list endpoint if you care.
* **Quota counts** `PENDING`, `ACCEPTED`, `DECLINED`, and `EXPIRED` invites. `REVOKED` invites are free. Let referrers revoke stale invites to recover quota.
* **Profile lookups from an ID token are cached for five minutes.** A status change that unlocks more invites can take that long to appear in `meta`.
* **Email address is the matching key at accept.** If the invitee joins with a different address, the referral is not credited.
* **The email is sent during the create request.** Response time includes email delivery.

See [Integrating Omneo Invites](/dev-guides/frontend/invites-plugin) for the recommended flow and [Configuring Invites](/experiences/invites/configuration) for the settings behind these behaviours.
