---
title: "Access keys and the API"
description: "Let your own site keep the people list: an access key lets your server add and remove people on one Invite only indicator as they pay and cancel."
order: 63
section: "sharing"
---

<!-- source: docs/indicators/sharing/access-keys-and-api.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Access keys and the API

Let your own site keep the people list: an access key lets your server add and remove people on one Invite only indicator as they pay and cancel.

## Make a key

In the People panel's corner menu, **Access key** opens the key sheet. Give
the key a label (where it lives: your site, a script) and press **Create
key**.

- The key belongs to your account and to this one indicator. It can add,
  remove and list people there, and nothing else.
- It is shown once. Copy it into your server's secret store.
- **Revoke** stops it at once: calls with it fail from then on.
- Make as many as you need, one per system, so each can be revoked on its
  own.

Keep the key on your server. Never put it in a web page, an app or a
public repository.

## The calls

Every call goes to your indicator's address on the registry and carries
the key as a bearer token:

```text
Base URL       https://registry.openmarket.xyz/v1/packages/@you/your-indicator
Authorization  Bearer <access key>

PUT     /access/<username>     add someone, or change their end date
DELETE  /access/<username>     remove someone
GET     /access/<username>     check one person
GET     /access                everyone with access, a page at a time
POST    /access/batch          up to 500 adds and removes in one call
POST    /access/links/batch    a personal link for each of your members
GET     /access/links.csv      the personal links nobody has opened yet
```

`@you/your-indicator` is your indicator's full name: your username, then
the name you published it under. The key sheet shows the address under
**Calls go to**.

## Add someone

```text
curl -X PUT "https://registry.openmarket.xyz/v1/packages/@you/your-indicator/access/maya" \
  -H "Authorization: Bearer $ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"until": "2027-01-01T00:00:00Z"}'
```

The body is optional: leave it out and access has no end date. A second
PUT replaces the end date, so a renewal sends the new one. The answer is
`200` with the person's row:

```json
{
  "username": "maya",
  "account_id": "<account id>",
  "until": "2027-01-01T00:00:00Z",
  "since": "2026-10-05T09:12:33Z",
  "via": "api"
}
```

Keep the `account_id`. It still finds the person after they rename their
account.

## Remove someone

```text
curl -X DELETE "https://registry.openmarket.xyz/v1/packages/@you/your-indicator/access/maya" \
  -H "Authorization: Bearer $ACCESS_KEY"
```

The answer is `204` whether or not they were on the list, so a
cancellation that arrives twice does no harm.

## Check and list

`GET /access/<username>` answers `200` with the person's row, or `404`
when they are not on the list.

`GET /access` reads everyone, 50 rows a page by default and up to 200 with
`limit`. Pass a page's `next_cursor` back as `cursor` to read the next
one. `q` filters by name, and `view` picks `all`, `ending` (access ending
soon) or `waiting` (personal links nobody has opened yet).

```text
curl "https://registry.openmarket.xyz/v1/packages/@you/your-indicator/access?limit=200&view=all" \
  -H "Authorization: Bearer $ACCESS_KEY"
```

## Many at once

`POST /access/batch` takes up to 500 adds and removes in one call: the
first import of your member list, or a nightly check that the list still
matches yours. Each `add` row works like a PUT (an `until` of `null` means
no end date), and each `remove` like a DELETE.

```text
curl -X POST "https://registry.openmarket.xyz/v1/packages/@you/your-indicator/access/batch" \
  -H "Authorization: Bearer $ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"add":[{"subject":"maya","until":null}],"remove":["sam"]}'
```

## Personal links

A member with no OpenMarket account yet gets a personal link instead. Ask
for one per member, keyed by your own id for them:

```text
curl -X POST "https://registry.openmarket.xyz/v1/packages/@you/your-indicator/access/links/batch" \
  -H "Authorization: Bearer $ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"links":[{"ref":"member-1042","until":null}]}'
```

Send each member their link yourself. When they open it, they sign in or
sign up and get access. `GET /access/links.csv` downloads the links nobody
has opened yet.

## Naming a person

Wherever a call takes a username, it also takes two other forms. In a
batch they go in `subject`.

| You write | Means |
| --- | --- |
| `maya` | an OpenMarket username |
| `id:<account id>` | the `account_id` a call returned; it still works after a rename |
| `ref:<your reference>` | your own id for a member you sent a personal link |

## Errors

An error comes back as `{"error": {"code": "...", "message": "..."}}`:

| Status | Code | What it means |
| --- | --- | --- |
| 404 | `user_not_found` | No one on OpenMarket has that username. |
| 422 | `not_invite_only` | The indicator is not Invite only. |
| 403 | `fair_use` | The Protected indicator is at 1,000 people ([Fair use](overview.md#fair-use)). |
| 429 | `rate_limited` | More than 600 writes in a minute on this key. Wait the seconds the `retry-after` header names. |

## Wire it to your billing

Two calls cover a membership, keyed by the OpenMarket username you collect
at checkout:

- **On signup**, `PUT /access/<username>`, with `until` set to the date
  they have paid through. Send it again on each renewal, and access lapses
  on its own if a renewal never comes.
- **On cancellation or refund**, `DELETE /access/<username>`.

No username at checkout? Make each customer a personal link instead, and
send it with your welcome email.

Selling through Whop, or behind a paid Discord role?
[Connect Discord or Whop](connect-discord-or-whop.md) has a small script for
each.

Selling with Stripe, on Patreon or through a paid Telegram group?
[Ready-made recipes](recipes.md) has a complete file for each, ready to run.

Webhooks get missed now and then. A nightly batch that compares the list
with your own member table catches whatever slipped through.
