# Admin panel API

The admin panel API gives your community's admin panel, game server mod or bot every ban on the SentinelShare network, kept up to date. Your tools decide what to do with them: flag, kick, or just show staff the history. It is read-only.

- Base URL: `https://sentinelshare.com/api/v1/integration`
- Auth: `Authorization: Bearer ssk_…` (a community key, see [Get a key](#get-a-key))
- Format: JSON, UTF-8. Times are ISO 8601 in UTC.

## Get a key

Keys belong to a community, not a person, so they keep working when staff change. A server owner creates them on SentinelShare under **API keys**. Make one per panel or tool, so you can revoke one without breaking the rest.

A key is shown once, when it's created. It can read every ban on the network, including players' IP addresses, and can't change anything. Keep it in your panel's server-side config (an environment variable or secrets file), never in browser code or a public repo. If one leaks, revoke it and create another.

## Authenticate

Send the key as a bearer token on every request:

```http
GET /api/v1/integration/bans HTTP/1.1
Host: sentinelshare.com
Authorization: Bearer ssk_your_key_here
```

A missing, wrong or revoked key gets `401`.

## Sync bans

`GET /bans` returns bans created or changed since your last call, oldest change first.

| Parameter | Meaning |
|---|---|
| `since` | The `next_cursor` from your previous response. Omit it for a full sync. |
| `limit` | Bans per page, 1 to 500. Default 200. |

The response:

```jsonc
{
  "items": [ /* ban records, see "The ban record" below */ ],
  "next_cursor": "MjAyNi0xMC0yNlQxNjowNjoyNS44MzgrMDA6MDB8NzRlOWM1NzA…",
  "has_more": false
}
```

How to use it:

1. **First run:** leave out `since`. Keep calling with the returned `next_cursor` until `has_more` is `false`. That pages through every ban on the network.
2. **Save `next_cursor`** somewhere that survives restarts (a database row or file).
3. **Every few minutes:** call with `since=<saved cursor>`. Usually you get nothing back and the same cursor.
4. **Store bans by `id`** and replace the copy you have when a ban comes back. A ban comes back whenever it changes: edited, lifted, verified or denied, or when its player gets a new identifier or is merged with another profile.

Changes show up in the feed a few seconds after they happen. A cursor never goes stale; if you lose it, drop `since` and sync from the start (records are replaced by `id`, so nothing duplicates).

## Look up a player

`GET /lookup` checks one player, for example when they join your server. Pass any identifiers you have; the Arma UID is the strongest match.

| Parameter | Meaning |
|---|---|
| `arma_uid` | Their Arma Reforger UID |
| `discord_id` | Their Discord user ID (the long number) |
| `ip` | An IP address they connected from |
| `name` | Part of their in-game name, at least 2 characters |

```bash
curl -G https://sentinelshare.com/api/v1/integration/lookup \
  -d arma_uid=f91f26e6-aea4-4822-b26b-c9135f3c3b66 \
  -H "Authorization: Bearer $SENTINEL_KEY"
```

```jsonc
{
  "found": true,
  "player": {
    "id": "b421bf42-0b9d-419d-a116-661852a6d496",
    "alias": "Halvorsen",
    "arma_uids": ["f91f26e6-aea4-4822-b26b-c9135f3c3b66"],
    "discord_ids": ["347530151542738677"],
    "ips": ["203.0.113.7"],
    "names": ["Halvorsen", "Halv"]
  },
  "url": "https://sentinelshare.com/subjects/b421bf42-0b9d-419d-a116-661852a6d496",
  "bans": [ /* every ban on this player, newest first, same shape as the feed */ ],
  "other_matches": [
    { "id": "4c49145e-…", "alias": "Halv0rsen", "url": "https://sentinelshare.com/subjects/4c49145e-…",
      "matched_on": ["ip"], "active_bans": 1 }
  ]
}
```

A player with no record returns `{"found": false, "player": null, "url": null, "bans": [], "other_matches": []}`.

When several profiles match, the best one is `player` and the rest are in `other_matches`, with `matched_on` saying how they matched.

## The ban record

The feed and lookup return bans in one shape, so you can store either. Fields are only ever added, never renamed or removed, so ignore fields you don't know.

```json
{
  "id": "239c7d3f-ccb3-4762-880b-bcc84d677c9b",
  "url": "https://sentinelshare.com/subjects/b421bf42-0b9d-419d-a116-661852a6d496",
  "status": "active",
  "type": "CHEATING",
  "reason": "Aimbot confirmed while spectating",
  "evidence": ["https://medal.tv/clips/…"],
  "filed_at": "2026-10-12T18:04:00Z",
  "filed_by": { "name": "Ryan", "community": "Iron Wolf Milsim" },
  "verification": {
    "state": "PEER_VERIFIED",
    "history": [
      { "action": "PEER_VERIFY", "by": "Mara", "community": "Northshore RP",
        "notes": "Same clip on our server", "at": "2026-10-13T09:12:44Z" }
    ]
  },
  "player": {
    "id": "b421bf42-0b9d-419d-a116-661852a6d496", "alias": "Halvorsen",
    "arma_uids": ["f91f26e6-…"], "discord_ids": ["3475…"],
    "ips": ["203.0.113.7"], "names": ["Halvorsen", "Halv"]
  },
  "other_matches": [
    { "id": "4c49145e-…", "alias": "Halv0rsen", "url": "https://sentinelshare.com/subjects/4c49145e-…",
      "matched_on": ["ip"], "active_bans": 1 }
  ],
  "updated_at": "2026-10-13T09:12:44Z"
}
```

| Field | Meaning |
|---|---|
| `id` | The ban's ID. Store bans by it; a changed ban comes back with the same ID. |
| `url` | The player's profile on SentinelShare. Opening it needs a staff sign-in. |
| `status` | `active`, or `lifted` when the ban was removed or an appeal was accepted. |
| `type` | `CHEATING`, `EXPLOITING`, `DUPLICATING`, `TOXICITY_TROLLING`, `RACISM` or `DDOS` (DDoS threats and doxxing). |
| `reason`, `evidence` | What the filing staff member wrote, and their evidence links. |
| `filed_at` | When the ban was filed. |
| `filed_by` | The staff member who filed it and their community. |
| `verification.state` | `UNVERIFIED`, `PEER_VERIFIED` (another community vouched), `PENDING_STAFF_REVIEW`, `STAFF_VERIFIED` or `DENIED`. |
| `verification.history` | Every verification step, oldest first. |
| `player` | Everything known about the player: Arma UIDs, Discord IDs, IP addresses and names, main one first. |
| `other_matches` | Other profiles that may be the same person. `arma_uid` / `discord_id`: flagged as a possible duplicate. `ip`: shares an IP address. Up to 20. |
| `updated_at` | When anything in this record last changed. |

## Deciding what to do with a ban

SentinelShare doesn't tell you to ban anyone; your community decides. Common rules:

- **Kick or block** players with an `active` ban whose verification is `PEER_VERIFIED` or `STAFF_VERIFIED`.
- **Flag for staff** players with an `active` but `UNVERIFIED` ban, or with `other_matches` that have active bans.
- **Ignore** `lifted` bans and anything `DENIED`, but keep them in your history.

## Limits and errors

Each key can make 300 requests a minute. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. Errors come back as JSON with a `detail` message.

| Status | Why | What to do |
|---|---|---|
| `401` | No key, a wrong key, a revoked key, or your community was deactivated | Check the key; ask your server owner for a new one |
| `400` | The `since` cursor isn't one we gave you | Drop `since` and sync from the start |
| `422` | A lookup without any identifier, or a value that's too long | Pass at least one of `arma_uid`, `discord_id`, `ip`, `name` |
| `429` | Over 300 requests a minute for this key | Wait the number of seconds in the `Retry-After` header |
| `5xx` | SentinelShare is having trouble | Retry with backoff; your stored bans still work |
