
# Create invitations in bulk

`POST /v1/invitations:batch` · permission `invitations:write`

Creates 1 to 100 invitations (`items`). Each item follows the rules of `POST /v1/invitations` and is written on its own: a failing item does not stop the others. The response is `200 OK` and `items` keeps the request order; an item's `status` is `201` (new invitation), `200` (a pending invitation was resent) or the HTTP status of its error, and a failed item carries `error` with `code`, `status` and field pointers of the form `/items/<i>/…`. The `accept_url` of `link_only` items is returned only in this response (`Cache-Control: no-store`); a response replayed with `Idempotency-Key` has no links and returns `secret_redacted: true`. An item without `organization_id` is an environment invitation. An `email` item is sent by e-mail; if invitation e-mail is not configured for the deployment it is a `501 not_implemented` item error, and an `application_id` that does not exist in this environment is a `404 not_found` item error. Each written invitation is audited as `invitation.created`.

## Request

| Field | Type | Required | Description |
|---|---|---|---|
| `items` | object[] | yes | 1 to 100 invitations, each with the fields of `POST /v1/invitations`. |
| `Idempotency-Key` | header | no | Makes a retry return the stored result without links. |

## Response

`200 OK` with one result per item.

```json
{
  "items": [
    {
      "status": 201,
      "invitation": {
        "id": "inv_01j9xt9v3w4y8z0a2b4c6d8e0g",
        "organization_id": "org_01j9xk3m7q8r2s4t6v8w0y2z4a",
        "email": "ada@example.com",
        "role_ids": [
          "rol_01j9xr7s1t2w6x8y0z2a4b6c8e"
        ],
        "application_id": null,
        "delivery": "link_only",
        "delivery_status": "not_sent",
        "status": "pending",
        "send_count": 1,
        "last_sent_at": "2026-10-12T09:31:00.123456Z",
        "expires_at": "2026-10-19T09:31:00.123456Z",
        "accepted_user_id": null,
        "accepted_at": null,
        "revoked_at": null,
        "created_at": "2026-10-12T09:31:00.123456Z",
        "updated_at": "2026-10-12T09:31:00.123456Z",
        "version": 1,
        "accept_url": "https://acme.eu.keenauth.app/u/login?invitation=<token>"
      }
    },
    {
      "status": 422,
      "error": {
        "code": "validation_failed",
        "status": 422,
        "errors": [
          {
            "pointer": "/items/1/email",
            "code": "invalid_value"
          }
        ]
      }
    }
  ]
}
```

## Errors

| HTTP | code | When |
|---|---|---|
| 422 | `validation_failed` | `items` is missing, empty or longer than 100. |
| 401 | `unauthenticated` | The credential is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The credential lacks the required permission. An item with a privileged (`is_privileged`) role fails with this item error when the credential lacks `roles:write`. |
| 429 | `rate_limited` | The rate limit was exceeded; retry after the `Retry-After` period. |
| 503 | `service_unavailable` | A dependency is temporarily unavailable; retry after the `Retry-After` period. |

## Example

```bash
curl -i -X POST "https://api.eu.keenauth.com/v1/invitations:batch" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0f6c1d2e3a4b5c6d7e8f9a0b" \
  -d '{"items":[{"organization_id":"org_01j9xk3m7q8r2s4t6v8w0y2z4a","email":"ada@example.com","delivery":"link_only"},{"organization_id":"org_01j9xk3m7q8r2s4t6v8w0y2z4a","email":"bob@example.com","delivery":"link_only"}]}'
```

## Personal data

This endpoint carries personal data: organization memberships and invitations. [Personal data in the KeenAuth APIs](/docs/policies/personal-data) describes what is stored, how long it is kept and how it is deleted. Do not send special categories of personal data (health, biometric data, religion, political opinions, sexual orientation) in any field.
