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:
- Open Indicators, then Mine, then the Indicator.
- In its people list, open the corner menu and choose Access key.
- 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. |
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
| Property | Detail |
|---|---|
| Base URL | https:/ (your Indicator's scope and name) |
| Auth | Authorization: Bearer <access key> on every request |
| Bodies | JSON, with Content-Type: application/ |
| 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:
{
"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:
| 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) 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
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. |
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"}'
{
"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.
| 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
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 -X DELETE "https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice" \ -H "Authorization: Bearer $OM_ACCESS_KEY"
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
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 "https://registry.openmarket.xyz/v1/packages/@scope/name/access/alice" \ -H "Authorization: Bearer $OM_ACCESS_KEY"
{
"username": "alice",
"account_id": "acc_8f2k1",
"until": "2026-11-04T00:00:00Z",
"since": "2026-10-04T09:12:33Z",
"via": "api"
}
List people
| 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 |
curl "https://registry.openmarket.xyz/v1/packages/@scope/name/access?limit=50&view=all" \ -H "Authorization: Bearer $OM_ACCESS_KEY"
{
"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
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. |
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"]}'
{
"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
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. |
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"}]}'
{
"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"
}
]
}
| 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.
Returns ref,url,state,until for every link that has not been opened yet, ready for a mail merge.
curl "https://registry.openmarket.xyz/v1/packages/@scope/name/access/links.csv" \ -H "Authorization: Bearer $OM_ACCESS_KEY"
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 -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 -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.
# 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 -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 "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 -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.