---
title: Access API
description: Keep the member list of an Invite-only Indicator in sync from your own membership system, server to server.
---

# Access API

An Indicator shared as **Invite only** is open to the people on its list and nobody else. The Access API lets your own server add, remove and list those people, so your membership site, a Whop or Stripe webhook, or a Discord bot keeps the list current without anyone editing it by hand.

It is a small API: one row per person, three ways to name a person, a batch call for the first import, and links for members who do not have an OpenMarket account yet.

## Access keys

Keys are made in the chart, on the Indicator they are for:

1. Open **Indicators**, then **Mine**, then the Indicator.
2. In its people list, open the corner menu and choose **Access key**.
3. Choose **Create key**. The key is shown once; copy it into your server's secret store.

Revoke a key from the same place.

| Property | Detail |
| --- | --- |
| **Format** | `omk_` followed by a long random string |
| **Bound to** | Your account and that one Indicator |
| **Can do** | Add, remove and list the people on that Indicator. Nothing else on your account. |
| **How many** | As many as you need. Make one key per system (one for the webhook job, one for the nightly job) so each can be revoked on its own. |
| **Shown** | Once, at creation. A lost key is revoked and replaced, never recovered. |

<Callout kind="warn">

Access keys are **server-side credentials**. Never ship one in a browser, a mobile app, or a public repository. If a key leaks, revoke it from the Indicator's people list and create a new one.

</Callout>

## Basics

| Property | Detail |
| --- | --- |
| **Base URL** | `https://registry.openmarket.xyz/v1/packages/@scope/name` (your Indicator's scope and name) |
| **Auth** | `Authorization: Bearer <access key>` on every request |
| **Bodies** | JSON, with `Content-Type: application/json` |
| **Errors** | `{ "error": { "code", "message" } }` with the matching HTTP status |
| **Paging** | Every list takes `limit` (1 to 200, default 50) and `cursor`, and returns `next_cursor` while there are more pages |
| **Repeats** | Every write is safe to repeat. A second identical PUT, DELETE or batch leaves the list as it was. |

Errors look like this:

<CodeBlock lang="JSON">
<pre>{
  <span class="s-p">"error"</span>: {
    <span class="s-p">"code"</span>: <span class="s-s">"user_not_found"</span>,
    <span class="s-p">"message"</span>: <span class="s-s">"No one on OpenMarket is called alicee"</span>
  }
}</pre>
</CodeBlock>

## Naming a person

Every per-person endpoint takes a `subject` in its path. Three forms are accepted:

| Subject | Example | Use when |
| --- | --- | --- |
| Username | `alice` | You collected the member's OpenMarket username (a purchase form, a Discord command) |
| `id:<account id>` | `id:acc_8f2k1` | You stored the `account_id` from an earlier response. Survives a username change. |
| `ref:<your member id>` | `ref:member-1042` | You handed this member a link (see [Members without an account yet](#members-without-an-openmarket-account-yet)) and only know your own id for them |

Rows are keyed to the account, not the username. A member who renames themselves keeps access, and `id:` and `ref:` keep finding them.

## Add or renew a person

<EndpointBar method="PUT" path="/access/{subject}"></EndpointBar>

Adds the person to the list, or updates their end date if they are already on it.

| Body field | Type | Description |
| --- | --- | --- |
| `until` | string, optional | ISO 8601 date-time when access ends. Omit it for no end date. A PUT **replaces** the end date, so a renewal simply sends the new one. |

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-k">-X</span> PUT <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Content-Type: application/json"</span> \
  <span class="s-k">-d</span> <span class="s-s">'{"until":"2026-11-04T00:00:00Z"}'</span></pre>
</CodeBlock>

<CodeBlock lang="JSON">
<pre>{
  <span class="s-p">"username"</span>: <span class="s-s">"alice"</span>,
  <span class="s-p">"account_id"</span>: <span class="s-s">"acc_8f2k1"</span>,
  <span class="s-p">"until"</span>: <span class="s-s">"2026-11-04T00:00:00Z"</span>,
  <span class="s-p">"since"</span>: <span class="s-s">"2026-10-04T09:12:33Z"</span>,
  <span class="s-p">"via"</span>: <span class="s-s">"api"</span>
}</pre>
</CodeBlock>

Keep the `account_id`. It is the one handle that still works after the member changes their username.

| Code | HTTP Status | Description |
| --- | --- | --- |
| `user_not_found` | 404 | No one on OpenMarket has that username. The message says so: "No one on OpenMarket is called …". Check spelling on your side before retrying. |
| `not_invite_only` | 422 | The Indicator is not shared as Invite only. Change its sharing in the chart first. |
| `rate_limited` | 429 | More than 600 writes in a minute on this key. Wait and retry. |
| `fair_use` | 403 | On a Protected Indicator the list is capped: "Fair use is 1,000 people. Talk to us for more." |

## Remove a person

<EndpointBar method="DELETE" path="/access/{subject}"></EndpointBar>

Ends the person's access. Returns `204` whether or not they were on the list, so a cancel webhook that fires twice is harmless.

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-k">-X</span> DELETE <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span></pre>
</CodeBlock>

<CodeBlock lang="HTTP Response">
<pre><span class="s-k">HTTP/1.1</span> <span class="s-n">204</span> <span class="s-v">No Content</span></pre>
</CodeBlock>

To remove someone whose username has changed since you added them, use the stored account id: `DELETE /access/id:acc_8f2k1`.

## Check one person

<EndpointBar method="GET" path="/access/{subject}"></EndpointBar>

Returns the person's row, or `404` if they are not on the list. It also answers "does this username exist?", which makes it a good check to run before a purchase form submits: a typo fails here, not after payment.

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span></pre>
</CodeBlock>

<CodeBlock lang="JSON">
<pre>{
  <span class="s-p">"username"</span>: <span class="s-s">"alice"</span>,
  <span class="s-p">"account_id"</span>: <span class="s-s">"acc_8f2k1"</span>,
  <span class="s-p">"until"</span>: <span class="s-s">"2026-11-04T00:00:00Z"</span>,
  <span class="s-p">"since"</span>: <span class="s-s">"2026-10-04T09:12:33Z"</span>,
  <span class="s-p">"via"</span>: <span class="s-s">"api"</span>
}</pre>
</CodeBlock>

## List people

<EndpointBar method="GET" path="/access"></EndpointBar>

| Query parameter | Default | Description |
| --- | --- | --- |
| `limit` | `50` | Rows per page, 1 to 200 |
| `cursor` | | The `next_cursor` from the previous page. Leave empty for the first page. |
| `q` | | Filter by username or ref |
| `view` | `all` | `all` for everyone, `ending` for people whose end date is coming up, `waiting` for links that have not been opened yet |

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access?limit=50&amp;view=all"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span></pre>
</CodeBlock>

<CodeBlock lang="JSON">
<pre>{
  <span class="s-p">"people"</span>: [
    {
      <span class="s-p">"username"</span>: <span class="s-s">"alice"</span>,
      <span class="s-p">"account_id"</span>: <span class="s-s">"acc_8f2k1"</span>,
      <span class="s-p">"until"</span>: <span class="s-s">"2026-11-04T00:00:00Z"</span>,
      <span class="s-p">"since"</span>: <span class="s-s">"2026-10-04T09:12:33Z"</span>,
      <span class="s-p">"via"</span>: <span class="s-s">"api"</span>,
      <span class="s-p">"state"</span>: <span class="s-s">"active"</span>
    }
  ],
  <span class="s-p">"total"</span>: <span class="s-n">212</span>,
  <span class="s-p">"ending_soon"</span>: <span class="s-n">9</span>,
  <span class="s-p">"waiting"</span>: <span class="s-n">3</span>,
  <span class="s-p">"next_cursor"</span>: <span class="s-s">"eyJhZnRlciI6ImFjY184ZjJrMSJ9"</span>
}</pre>
</CodeBlock>

`total`, `ending_soon` and `waiting` count the whole list, not the page. Pass `next_cursor` back as `cursor` until no `next_cursor` comes back.

## Batch changes

<EndpointBar method="POST" path="/access/batch"></EndpointBar>

Adds, renews and removes up to 500 people in one call. Use it for the first import of an existing member list and for the nightly reconcile below.

| Body field | Description |
| --- | --- |
| `add` | Rows of `{ "subject", "until" }`. Same meaning as a PUT per row; `until` null means no end date. |
| `remove` | Subjects to remove. Same meaning as a DELETE per subject. |

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-k">-X</span> POST <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/batch"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Content-Type: application/json"</span> \
  <span class="s-k">-d</span> <span class="s-s">'{"add":[{"subject":"alice","until":null},{"subject":"carol","until":"2026-12-01T00:00:00Z"}],"remove":["bob"]}'</span></pre>
</CodeBlock>

<CodeBlock lang="JSON">
<pre>{
  <span class="s-p">"added"</span>: <span class="s-n">1</span>,
  <span class="s-p">"updated"</span>: <span class="s-n">1</span>,
  <span class="s-p">"removed"</span>: <span class="s-n">1</span>,
  <span class="s-p">"not_found"</span>: [],
  <span class="s-p">"refused"</span>: []
}</pre>
</CodeBlock>

A batch does not stop at the first bad row. Usernames that do not exist come back in `not_found`, and rows the Indicator refused come back in `refused` as `{ "subject", "code" }` with the same codes as a PUT. Everything else is applied, and the call is safe to repeat.

## Members without an OpenMarket account yet

<EndpointBar method="POST" path="/access/links/batch"></EndpointBar>

You do not have to collect usernames at all. Create a link per member, keyed by your own member id, and hand each member their link in your welcome email or on your members page. Access starts the moment they open it signed in, or sign up from it, and the row shows their username from then on. No usernames to collect, no typos.

| Body field | Description |
| --- | --- |
| `links` | Up to 500 rows of `{ "ref", "until" }`. `ref` is your own id for the member; `until` works as in a PUT. |

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-k">-X</span> POST <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/links/batch"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Content-Type: application/json"</span> \
  <span class="s-k">-d</span> <span class="s-s">'{"links":[{"ref":"member-1042","until":null},{"ref":"member-1043","until":"2026-12-01T00:00:00Z"}]}'</span></pre>
</CodeBlock>

<CodeBlock lang="JSON">
<pre>{
  <span class="s-p">"links"</span>: [
    {
      <span class="s-p">"ref"</span>: <span class="s-s">"member-1042"</span>,
      <span class="s-p">"url"</span>: <span class="s-s">"https://openmarket.xyz/join/k7Qm2pX9"</span>,
      <span class="s-p">"state"</span>: <span class="s-s">"waiting"</span>
    },
    {
      <span class="s-p">"ref"</span>: <span class="s-s">"member-1043"</span>,
      <span class="s-p">"url"</span>: <span class="s-s">"https://openmarket.xyz/join/b3Nw8rT2"</span>,
      <span class="s-p">"state"</span>: <span class="s-s">"joined"</span>,
      <span class="s-p">"username"</span>: <span class="s-s">"dave"</span>
    }
  ]
}</pre>
</CodeBlock>

| Field | Description |
| --- | --- |
| `url` | The link to give that member. The same `ref` always returns the same URL, so it is safe to call again when you resend an email. |
| `state` | `waiting` until the member opens the link, `joined` after. |
| `username` | Present once the state is `joined`. |

From then on the member is addressed as `ref:<your id>` on every endpoint. When their membership ends: `DELETE /access/ref:member-1042`.

<EndpointBar method="GET" path="/access/links.csv"></EndpointBar>

Returns `ref,url,state,until` for every link that has not been opened yet, ready for a mail merge.

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/links.csv"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span></pre>
</CodeBlock>

<CodeBlock lang="CSV">
<pre>ref,url,state,until
member-1042,https://openmarket.xyz/join/k7Qm2pX9,waiting,
member-1051,https://openmarket.xyz/join/p2Vd4sL6,waiting,2026-12-01T00:00:00Z</pre>
</CodeBlock>

## When access ends

- A removed member's server-side (Protected) run stops within about 15 minutes.
- A Compiled module that a member's browser has already downloaded cannot be recalled. It stops updating; it does not disappear from a page that is already open.

## Sync patterns

A membership platform's webhooks (new member, cancelled member, failed payment) are the usual trigger. Each pattern is one call.

### On purchase: PUT with the paid-through date

Send `until` as the date the member has paid through, and send it again on every renewal payment. If a renewal webhook never arrives, access lapses on its own instead of staying open.

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-k">-X</span> PUT <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Content-Type: application/json"</span> \
  <span class="s-k">-d</span> <span class="s-s">'{"until":"2026-11-04T00:00:00Z"}'</span></pre>
</CodeBlock>

### On cancel or refund: DELETE

Fire it from the cancellation or refund webhook. A repeat is harmless (`204` either way).

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-k">-X</span> DELETE <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span></pre>
</CodeBlock>

### Nightly reconcile: list, compare, batch

Webhooks get missed. Once a night, page through the list, compare it with your own member table, and send one batch that adds who is missing and removes who should be gone.

<CodeBlock lang="cURL">
<pre><span class="s-c"># 1. Page through everyone on the list (repeat with cursor=next_cursor while one comes back)</span>
<span class="s-f">curl</span> <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access?limit=200"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span>
&#10;
<span class="s-c"># 2. Send the difference in one batch</span>
<span class="s-f">curl</span> <span class="s-k">-X</span> POST <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/batch"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Content-Type: application/json"</span> \
  <span class="s-k">-d</span> <span class="s-s">'{"add":[{"subject":"erin","until":"2026-11-30T00:00:00Z"}],"remove":["id:acc_3jd9q"]}'</span></pre>
</CodeBlock>

## Try it

Four calls, start to finish, on your own account.

**Step 1: create a key.** Open one of your Invite-only Indicators, create a key as described under [Access keys](#access-keys), and put it in `OM_ACCESS_KEY`.

**Step 2: add yourself.** No body means no end date.

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-k">-X</span> PUT <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/YOUR_USERNAME"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span></pre>
</CodeBlock>

**Step 3: read the row back.** It matches what the PUT returned.

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/YOUR_USERNAME"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span></pre>
</CodeBlock>

**Step 4: remove it again.** The answer is `204`, and a second DELETE answers `204` too.

<CodeBlock lang="cURL">
<pre><span class="s-f">curl</span> <span class="s-k">-X</span> DELETE <span class="s-s">"https://registry.openmarket.xyz/v1/packages/@scope/name/access/YOUR_USERNAME"</span> \
  <span class="s-k">-H</span> <span class="s-s">"Authorization: Bearer $OM_ACCESS_KEY"</span></pre>
</CodeBlock>

That is the whole loop a webhook handler needs.
