Access API

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

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.

PropertyDetail
Formatomk_ followed by a long random string
Bound toYour account and that one Indicator
Can doAdd, remove and list the people on that Indicator. Nothing else on your account.
How manyAs 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.
ShownOnce, at creation. A lost key is revoked and replaced, never recovered.

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.

Basics

PropertyDetail
Base URLhttps://registry.openmarket.xyz/v1/packages/@scope/name (your Indicator's scope and name)
AuthAuthorization: Bearer <access key> on every request
BodiesJSON, with Content-Type: application/json
Errors{ "error": { "code", "message" } } with the matching HTTP status
PagingEvery list takes limit (1 to 200, default 50) and cursor, and returns next_cursor while there are more pages
RepeatsEvery write is safe to repeat. A second identical PUT, DELETE or batch leaves the list as it was.

Errors look like this:

JSON
{
  "error": {
    "code": "user_not_found",
    "message": "No one on OpenMarket is called alicee"
  }
}

Naming a person

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

SubjectExampleUse when
UsernamealiceYou collected the member's OpenMarket username (a purchase form, a Discord command)
id:<account id>id:acc_8f2k1You stored the account_id from an earlier response. Survives a username change.
ref:<your member id>ref:member-1042You handed this member a link (see Members without an 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

PUT/access/{subject}

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

Body fieldTypeDescription
untilstring, optionalISO 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.
cURL
curl -X PUT "https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice" \
  -H "Authorization: Bearer $OM_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"until":"2026-11-04T00:00:00Z"}'
JSON
{
  "username": "alice",
  "account_id": "acc_8f2k1",
  "until": "2026-11-04T00:00:00Z",
  "since": "2026-10-04T09:12:33Z",
  "via": "api"
}

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

CodeHTTP StatusDescription
user_not_found404No 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_only422The Indicator is not shared as Invite only. Change its sharing in the chart first.
rate_limited429More than 600 writes in a minute on this key. Wait and retry.
fair_use403On a Protected Indicator the list is capped: "Fair use is 1,000 people. Talk to us for more."

Remove a person

DELETE/access/{subject}

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

cURL
curl -X DELETE "https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice" \
  -H "Authorization: Bearer $OM_ACCESS_KEY"
HTTP Response
HTTP/1.1 204 No Content

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

Check one person

GET/access/{subject}

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.

cURL
curl "https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice" \
  -H "Authorization: Bearer $OM_ACCESS_KEY"
JSON
{
  "username": "alice",
  "account_id": "acc_8f2k1",
  "until": "2026-11-04T00:00:00Z",
  "since": "2026-10-04T09:12:33Z",
  "via": "api"
}

List people

GET/access

Query parameterDefaultDescription
limit50Rows per page, 1 to 200
cursorThe next_cursor from the previous page. Leave empty for the first page.
qFilter by username or ref
viewallall for everyone, ending for people whose end date is coming up, waiting for links that have not been opened yet
cURL
curl "https://registry.openmarket.xyz/v1/packages/@scope/name/access?limit=50&view=all" \
  -H "Authorization: Bearer $OM_ACCESS_KEY"
JSON
{
  "people": [
    {
      "username": "alice",
      "account_id": "acc_8f2k1",
      "until": "2026-11-04T00:00:00Z",
      "since": "2026-10-04T09:12:33Z",
      "via": "api",
      "state": "active"
    }
  ],
  "total": 212,
  "ending_soon": 9,
  "waiting": 3,
  "next_cursor": "eyJhZnRlciI6ImFjY184ZjJrMSJ9"
}

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

POST/access/batch

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 fieldDescription
addRows of { "subject", "until" }. Same meaning as a PUT per row; until null means no end date.
removeSubjects to remove. Same meaning as a DELETE per subject.
cURL
curl -X POST "https://registry.openmarket.xyz/v1/packages/@scope/name/access/batch" \
  -H "Authorization: Bearer $OM_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"add":[{"subject":"alice","until":null},{"subject":"carol","until":"2026-12-01T00:00:00Z"}],"remove":["bob"]}'
JSON
{
  "added": 1,
  "updated": 1,
  "removed": 1,
  "not_found": [],
  "refused": []
}

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

POST/access/links/batch

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 fieldDescription
linksUp to 500 rows of { "ref", "until" }. ref is your own id for the member; until works as in a PUT.
cURL
curl -X POST "https://registry.openmarket.xyz/v1/packages/@scope/name/access/links/batch" \
  -H "Authorization: Bearer $OM_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"links":[{"ref":"member-1042","until":null},{"ref":"member-1043","until":"2026-12-01T00:00:00Z"}]}'
JSON
{
  "links": [
    {
      "ref": "member-1042",
      "url": "https://openmarket.xyz/join/k7Qm2pX9",
      "state": "waiting"
    },
    {
      "ref": "member-1043",
      "url": "https://openmarket.xyz/join/b3Nw8rT2",
      "state": "joined",
      "username": "dave"
    }
  ]
}
FieldDescription
urlThe 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.
statewaiting until the member opens the link, joined after.
usernamePresent 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.

GET/access/links.csv

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

cURL
curl "https://registry.openmarket.xyz/v1/packages/@scope/name/access/links.csv" \
  -H "Authorization: Bearer $OM_ACCESS_KEY"
CSV
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

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.

cURL
curl -X PUT "https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice" \
  -H "Authorization: Bearer $OM_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"until":"2026-11-04T00:00:00Z"}'

On cancel or refund: DELETE

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

cURL
curl -X DELETE "https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice" \
  -H "Authorization: Bearer $OM_ACCESS_KEY"

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.

cURL
# 1. Page through everyone on the list (repeat with cursor=next_cursor while one comes back)
curl "https://registry.openmarket.xyz/v1/packages/@scope/name/access?limit=200" \
  -H "Authorization: Bearer $OM_ACCESS_KEY"


# 2. Send the difference in one batch
curl -X POST "https://registry.openmarket.xyz/v1/packages/@scope/name/access/batch" \
  -H "Authorization: Bearer $OM_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"add":[{"subject":"erin","until":"2026-11-30T00:00:00Z"}],"remove":["id:acc_3jd9q"]}'

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, and put it in OM_ACCESS_KEY.

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

cURL
curl -X PUT "https://registry.openmarket.xyz/v1/packages/@scope/name/access/YOUR_USERNAME" \
  -H "Authorization: Bearer $OM_ACCESS_KEY"

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

cURL
curl "https://registry.openmarket.xyz/v1/packages/@scope/name/access/YOUR_USERNAME" \
  -H "Authorization: Bearer $OM_ACCESS_KEY"

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

cURL
curl -X DELETE "https://registry.openmarket.xyz/v1/packages/@scope/name/access/YOUR_USERNAME" \
  -H "Authorization: Bearer $OM_ACCESS_KEY"

That is the whole loop a webhook handler needs.