# Organization API

> The key-authenticated /api/v1/org/* API for servers and SDKs acting as the whole organization: list/create/revoke any pass, list facilities, and read the member directory. Scopes org:passes:*, org:facilities:read and org:members:*.

The organization API is the **server-to-server** data plane: endpoints for a backend or SDK acting as the **whole organization** rather than a single signed-in user. Authenticate with an [organization API key](/docs/api-keys/overview/) and `org:*` [scopes](/docs/oauth/scopes/).

**Base:** `https://entrybit.net` · **Auth:** `Authorization: Bearer eb_sk_…` (or `X-API-Key`) · see [Authenticating requests](/docs/api-keys/authenticating/).

It mirrors the [user-delegated passes API](/docs/api-reference/passes/), but organization-wide: it lists and revokes **any** pass in the company, adds a facilities endpoint SDKs need to create passes, and exposes a read-only **[member directory](#list-members)** (who is in the organization). Everything stays tenant-scoped — a key only ever touches its own organization's data.

## List all passes

```http
GET /api/v1/org/passes?limit&cursor&search
```

**Scope:** `org:passes:read`. Every non-deleted pass in the organization, keyset-paginated newest first. Rows include `created_by` (the inviting user's id) for attribution.

Parameters: `limit` (1–100, default 30), `cursor`, `search` — see [pagination](/docs/api-reference/conventions/).

```bash
curl "https://entrybit.net/api/v1/org/passes?limit=50" \
  -H "Authorization: Bearer $ENTRYBIT_API_KEY"
```

```json
{
  "success": true,
  "items": [
    { "public_id": "gst_9f1c…", "first_name": "Dana", "status": "checked_in", "arrival_date": "2026-07-12", "facility_name": "North Gate", "created_by": 90210 }
  ],
  "total": 340,
  "next_cursor": "eyJpZCI6…",
  "has_more": true
}
```

The [Pass object](/docs/api-reference/passes/) is identical to the user-delegated API. (The `usage` allowance block is a user-delegated concept and does not appear here.)

## Create pass(es)

```http
POST /api/v1/org/passes
Content-Type: application/json
```

**Scope:** `org:passes:write`. Creates 1–10 passes with the **same request body, validation, quota, QR and delivery** as [`POST /api/v1/passes`](/docs/api-reference/passes/). Attribution and the invite quota go to the admin who minted the key.

```bash
curl -X POST https://entrybit.net/api/v1/org/passes \
  -H "Authorization: Bearer $ENTRYBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Dana",
    "email": "dana@example.com",
    "arrival_date": "2026-07-12",
    "facility_id": 42,
    "quantity": 1
  }'
```

Returns `201` with the same [`PassCreateResponse`](/docs/api-reference/passes/) shape (`public_id`, `qr_values`, `pass_link`, `qr_sent`, `sms_sent`, …). Errors: `400` validation, `402` quota, `404` facility not found, `409` facility deactivated or suspended.

## Revoke any pass

```http
DELETE /api/v1/org/passes/{public_id}
```

**Scope:** `org:passes:write`. Cancels the pass and deactivates its QR credential — **regardless of which user created it** (still scoped to the key's organization).

```json
{ "success": true, "public_id": "gst_9f1c2ab34cd56ef7", "revoked": true }
```

Returns `409` when the pass is no longer active, `404` when no such pass exists in the organization.

## List facilities

```http
GET /api/v1/org/facilities
```

**Scope:** `org:facilities:read`. The facility ids an SDK needs before it can create passes.

```bash
curl https://entrybit.net/api/v1/org/facilities \
  -H "Authorization: Bearer $ENTRYBIT_API_KEY"
```

```json
{
  "success": true,
  "facilities": [
    { "id": 42, "name": "North Gate", "description": null, "is_active": true, "suspended": false }
  ]
}
```

### The Facility object

| Field | Type | Notes |
|---|---|---|
| `id` | integer | Use as `facility_id` when creating passes. |
| `name` | string | |
| `description` | string \| null | |
| `is_active` | boolean | |
| `suspended` | boolean | Suspended facilities pause new invites. |

## List members

```http
GET /api/v1/org/members?limit&cursor&search&fields&status
```

**Scope:** `org:members:read` — plus `org:members:contact:read` to read contact fields. The organization's member directory, keyset-paginated newest first. Active members only by default — pass `status=inactive` or `status=all` to include deactivated members. Guest shadow rows never appear.

Parameters: `limit` (1–100, default 30), `cursor` — see [pagination](/docs/api-reference/conventions/); `search` — case-insensitive match on the member's name (and on email/phone when the credential carries the contact scope); `fields` — a comma-separated subset of the Member object (see [field selection](#field-selection-and-tiers)); `status` — `active` (default), `inactive`, or `all`.

```bash
curl "https://entrybit.net/api/v1/org/members?limit=50&fields=id,name,job_title,email" \
  -H "Authorization: Bearer $ENTRYBIT_API_KEY"
```

```json
{
  "success": true,
  "items": [
    { "id": "emp_7b2d1ac98ef04c35", "name": "Dana Cohen", "job_title": "Facilities Manager", "email": "dana@example.com" }
  ],
  "total": 87,
  "next_cursor": "eyJpZCI6…",
  "has_more": true
}
```

## Get a member

```http
GET /api/v1/org/members/{public_id}
```

**Scope:** `org:members:read`. One member by public id (`emp_…`) — `404` when no such member exists in the organization. Deactivated members **are** returned here (with `"status": "inactive"`) — their id stays addressable, Directory-style. Accepts the same `fields` parameter.

```json
{ "success": true, "member": { "id": "emp_7b2d1ac98ef04c35", "name": "Dana Cohen", "first_name": "Dana", "last_name": "Cohen", "job_title": "Facilities Manager", "department": "Operations", "status": "active", "created_at": "2026-05-19T09:30:00" } }
```

### The Member object

| Field | Tier | Type | Notes |
|---|---|---|---|
| `id` | basic | string | Public identifier, e.g. `emp_7b2d1ac98ef04c35`. |
| `name` | basic | string | Display name (first + last). |
| `first_name` | basic | string | |
| `last_name` | basic | string | |
| `job_title` | basic | string \| null | |
| `department` | basic | string \| null | The department's display name. |
| `status` | basic | string | `active` or `inactive`. |
| `created_at` | basic | string \| null | |
| `email` | contact | string \| null | Requires `org:members:contact:read`. |
| `phone` | contact | string \| null | Requires `org:members:contact:read`. |

Contact fields are stored **encrypted at rest** and decrypted server-side only when the credential carries the contact scope. The API never returns credential material, photo URLs, or internal indexes.

### Field selection and tiers

Graph-style sparse fieldsets: pass `fields` as a comma-separated subset of the table above. Omit it and you get **every field your credential's tier allows** — the basic tier with `org:members:read`, plus the contact tier when the key also carries `org:members:contact:read`.

- An **unknown field** returns `400`, and the message lists the allowed fields.
- A **contact field on a basic-only key** returns `403 insufficient_scope`, and the challenge names the missing scope:

```bash
# Key carries org:members:read only — email is a contact-tier field
curl "https://entrybit.net/api/v1/org/members?fields=id,name,email" \
  -H "Authorization: Bearer $ENTRYBIT_API_KEY"
```

```http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="org:members:contact:read"

{ "error": "insufficient_scope" }
```

### Signed-in access — the `members:read` scope

Both member endpoints also accept a user-delegated [OAuth access token](/docs/oauth/token/) carrying the `members:read` [scope](/docs/oauth/scopes/) — for apps a member signs into that need the directory without a server-side key. The call is then double-gated: the token must carry the scope **and** the signed-in user must still hold the organization's employee-view role — a removed user cannot keep reading the directory through an old grant. The response is scoped to the user's own organization, and `members:read` unlocks the full field set (the user's role already grants the in-app member list).

## Scope summary

| Endpoint | Scope |
|---|---|
| `GET /api/v1/org/passes` | `org:passes:read` |
| `POST /api/v1/org/passes` | `org:passes:write` |
| `DELETE /api/v1/org/passes/{public_id}` | `org:passes:write` |
| `GET /api/v1/org/facilities` | `org:facilities:read` |
| `GET /api/v1/org/members` | `org:members:read` (+ `org:members:contact:read` for contact fields) |
| `GET /api/v1/org/members/{public_id}` | `org:members:read` (+ `org:members:contact:read` for contact fields) |

Auth failures (`401`/`403`/`429`) follow the [API-key challenge](/docs/api-keys/authenticating/) rules; business errors follow the [error catalog](/docs/api-reference/errors/).