The SC Orgs API: keys, requests and webhooks

Give your org's bot or dashboard an API key, read members and events, and get signed webhooks when members join or an op is about to start.

Updated 2026-10-29

The SC Orgs API lets a bot, a dashboard or another tool read your org: its profile, members, roles, events and who signed up. It can also call your server the moment someone joins or an op is about to start (webhooks). You make an API key for it in Manage › API, and the key works for your org only. The API is read-only for now.

You need the Manage API Keys permission. The owner has it; nobody else does until the owner gives it to a role (see Advanced). The full reference and more code examples are on the developer docs.

Try it on the sandbox

You can try the API before making a key. ~SANDBOX is a made-up test org with a few members, roles and upcoming events, and its read-only key is public:

curl https://api.sc-orgs.space/api/v1/me \ -H "Authorization: Bearer sco_test_hKI7h4mK1eSEen1hfCCBUXOSQnKxIFbHSPcPM7sXVAl"

The answer names the org (~SANDBOX) and the key's scopes. Swap /me for /orgs/~SANDBOX/members or /orgs/~SANDBOX/events to see what a real key returns.

Create a key

For your security, creating a key needs a recent sign-in. If you use two-factor sign-in, you're asked for a code. Otherwise, if your last sign-in was more than 10 minutes ago, click Sign in again and try once more.

Make your first request

Every request goes to https://api.sc-orgs.space/api/v1 and sends the key in a header: Authorization: Bearer sco_live_…. Keep the key on your server: never put it in a web page or an app people download.

curl https://api.sc-orgs.space/api/v1/orgs/YOURSID/members \ -H "Authorization: Bearer sco_live_…"

Every answer is JSON, inside data. Lists come 25 at a time; ask for more with page and page_size (up to 100). Each list says how many there are in total.

Add a webhook

A webhook is a web address on your server that SC Orgs calls when something happens.

Your server should answer with a 2xx status within 10 seconds. Delivery log shows every delivery of the last 30 days, and Redeliver sends one again. Check the signature of every delivery before you trust it: see Advanced.

Advanced

Scopes and who can manage keys

Endpoints

All of them are GET, under https://api.sc-orgs.space/api/v1. The OpenAPI spec describes each one, with examples; you can import it into Postman or a code generator.

With a key, {org} must be the key's own org. Any other org answers 404, even a real one, so a key never learns what another org has.

Calls without a key

An org's profile and its public events (/orgs/{org}, /orgs/{org}/events and one event) also work with no key at all, so your org's website can show them. Those answers can be read from any website (CORS). Answers to requests with a key can't, because a key in a web page is a key anyone can copy. A request with a bad key is refused, never treated as a call without a key.

Paging

Errors

Errors look like this, whatever the endpoint:

{ "error": { "code": "INSUFFICIENT_SCOPE", "message": "This key lacks the rsvps:read scope", "details": { "required": ["events:read", "rsvps:read"], "effective": ["org:read", "events:read"] } } }

Rate limits

Answers carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (when the limit is full again, in unix seconds). When you get a 429, wait the Retry-After seconds. A key that gets 100 403 answers in an hour is suspended, because it's clearly set up wrong.

Webhook deliveries

Each delivery is a POST with a JSON body like this:

{ "id": "evt_…", "type": "member.joined", "api_version": "v1", "occurred_at": "2026-10-27T18:04:11.000Z", "organization": { "id": "4b2c…", "sid": "HRMS" }, "data": { "member": { "id": "c4a8…", "rsi_handle": "carol", … }, "via": "invite" } }

Check the signature of every webhook

Each delivery has webhook-id, webhook-timestamp and webhook-signature headers (Standard Webhooks). The signature is v1, and then the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with your secret: the part after whsec_, base64-decoded. Check it against the body exactly as it arrived, before you parse the JSON. The standardwebhooks library for Node, Python and other languages does the check for you.

JavaScript (Node, Express):

// npm install standardwebhooks express import express from 'express'; import { Webhook } from 'standardwebhooks'; const wh = new Webhook(process.env.SCO_WEBHOOK_SECRET); // whsec_… const app = express(); // express.raw keeps the body exactly as it arrived. app.post('/sco-hook', express.raw({ type: 'application/json' }), (req, res) => { let event; try { event = wh.verify(req.body.toString('utf8'), req.headers); // throws if it isn't from SC Orgs } catch { return res.status(400).end(); } if (event.type === 'member.joined') console.log('Welcome', event.data.member.rsi_handle); res.status(204).end(); // answer fast; do slow work afterwards }); app.listen(3000);

Python (Flask):

# pip install standardwebhooks flask import os from flask import Flask, request from standardwebhooks import Webhook wh = Webhook(os.environ["SCO_WEBHOOK_SECRET"]) # whsec_… app = Flask(__name__) @app.post("/sco-hook") def hook(): try: event = wh.verify(request.get_data(), dict(request.headers)) except Exception: return "", 400 if event["type"] == "event.starting": print(event["data"]["event"]["title"], "starts in", event["data"]["starts_in_seconds"], "seconds") return "", 204

Python without a library (the same check, step by step):

import base64, hashlib, hmac, time def verify(secret, headers, raw_body): # raw_body: bytes, as received headers = {k.lower(): v for k, v in headers.items()} msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"] if abs(time.time() - int(ts)) > 300: # more than 5 minutes off: reject return False key = base64.b64decode(secret.removeprefix("whsec_")) expected = hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest() return any( # two signatures while a secret rotates hmac.compare_digest(base64.b64decode(sig.split(",", 1)[1]), expected) for sig in headers["webhook-signature"].split(" ") if sig.startswith("v1,") )

curl: send your own server a signed test delivery, to check your code before you go live. It signs the body with your secret the way SC Orgs does (it needs openssl and xxd):

SECRET='whsec_…' URL='http://localhost:3000/sco-hook' BODY='{"id":"evt_test","type":"ping","api_version":"v1","occurred_at":"2026-10-28T12:00:00.000Z","organization":{"id":"test","sid":"TEST"},"data":{"message":"Hello from SC Orgs"}}' ID="msg_test_$(date +%s)"; TS=$(date +%s) KEY=$(printf '%s' "${SECRET#whsec_}" | openssl base64 -d -A | xxd -p -c 256) SIG=$(printf '%s' "$ID.$TS.$BODY" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY" -binary | openssl base64 -A) curl -i "$URL" -H 'content-type: application/json' \ -H "webhook-id: $ID" -H "webhook-timestamp: $TS" -H "webhook-signature: v1,$SIG" \ --data-raw "$BODY"

Your server should accept it. Change one letter of BODY after signing, and it should answer 400.

Retries and turning off

A delivery that doesn't get a 2xx within 10 seconds is tried again after about 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours. After 20 failures in a row, or 3 days of failures, the webhook turns off and key managers are told; fix your server, then click Turn on. A 410 Gone answer turns it off at once. Revoking the key turns off its webhooks, and while a key is suspended its webhooks get nothing.

Test orgs and keys

Keys of test orgs like ~SANDBOX start with sco_test_ and can never see real orgs or members. Test orgs are invisible to everyone else, calls without a key included. The sandbox data is reset now and then.

What the API never shares

Citizen Intel, HR records and application answers, members' live in-game location, Discord ids, email addresses, last activity, and account or two-factor details. Members who hide their membership stay hidden from the public, but your own org's keys see them.

Versions

The version is in the path (/api/v1). Within v1, changes only add fields, endpoints and events, so ignore fields you don't know. Anything that would break your code comes as a new version.

Bots on Discord

The API pairs well with the SC Orgs bot for your server: see Connect your Discord server.

All guides

Start here

Getting started

Run your org

Orgs

Members & roles

Events & ops

Discord

Treasury

API & webhooks

Your account

Account