Authentication

How Arkanis API tokens work: scoping, capability inheritance, rotation, expiry, and revocation. The same token model powers REST and MCP.

What is this?

One arkpat_ token shape powers both REST and MCP. Each token is bound to one guild, carries a snapshot of the issuer's capabilities, and is re-checked live against the issuer's current capabilities on every call. You can also pin a token to an IP allowlist and see rotation reminders at 90 and 180 days.

Why you might want it

This is the reference for everything token-related: how scoping works, what the live re-check catches, when auto-revoke fires, how the 7/30/90-day expiry options compare, and when to renew vs rotate vs revoke. Reach for it whenever a token misbehaves or you want to tighten an integration's blast radius before shipping it.

Happy path

What this is for

Minting an Arkanis personal access token (arkpat_) for a script, integration, CI job, or MCP client. One token shape, scoped to one guild, capability-bound to whoever issued it.

Before you start

  • ·Your guild is on the Pro tier.
  • ·You are the Discord guild owner (the API & MCP mint surface is owner-gated).
  • ·You have flipped Developer Mode on under Settings Panel. The toggle is per-browser; do it once on the machine you mint from.
  • ·You have a secret store ready (1Password, Vault, GitHub Actions secrets, etc.). The plaintext is shown once.

5-minute setup

  1. 1Open the dashboard for your guild → Settings → API & MCP.
  2. 2Click Create token on the REST or MCP tab, name it after the integration that will hold it (e.g. CI deploy bot).
  3. 3Pick the capabilities the integration needs. Default inherits your full set; narrow it down to the smallest workable subset.
  4. 4Optional but recommended for production: add a CIDR allowlist so the token only works from your CI egress or jump host.
  5. 5Pick an expiry (7d / 30d / 90d / never). 90d is the default and the right answer for most integrations.
  6. 6Copy the plaintext into your secret store now. It starts with arkpat_ and is never shown again.

Common failure modes

  • Calls suddenly return CAPABILITY_DENIED
    The issuing user lost a capability or got demoted. The live re-check intersects snapshot ∩ live on every call, so a role demotion takes effect immediately. Restore the capability on the issuing user, or mint a fresh token from someone who still has it.
  • Calls return TOKEN_IP_NOT_ALLOWED from a known-good source
    The caller's public IP is not inside any CIDR on the allowlist. NAT or IPv6 dual-stack often surprises here. Check your egress IP from the call site (curl -s https://ifconfig.me), then edit the allowlist on the token row to match the real source.
  • "Invalid token format" 401 on the first call
    The token shape is wrong. Bearer tokens must start with arkpat_ followed by 48 chars (55 total). Confirm there's no leading whitespace, no trailing newline from a copy, and no quoting from a shell expansion.
  • Lost the plaintext, can't find it anywhere
    There is no recovery flow by design. Revoke the row from the dashboard and mint a new token. Update the integration with the new plaintext.
Token model, capability snapshot, live re-check, IP allowlist, rotation cadence, auto-revoke, and lifecycle (renew / rotate / revoke) are documented below.

One Token Model, Two Surfaces

Both the REST API and the MCP server use the same token type: an Arkanis Personal Access Token (arkpat_). One row in the api_tokens table, one hash, one audit lineage. The token carries a kind discriminator ("mcp" or "rest") but the wire format, scoping rules, and lifecycle are identical.

  • Format: opaque random, prefix arkpat_ + 48 chars Crockford base32 (no I, L, O, or U). Total length 55 chars.
  • Entropy: 240 bits, generated server-side.
  • Storage: argon2id hash on disk. The plaintext is shown once at issuance or rotation and never again.
  • Leak detection: the arkpat_ prefix is registered with secret-scanners (GitHub, gitleaks) so a leaked token can be auto-detected.
ℹ️
Note
See spec §4 for the full authentication model. This page is the customer-facing summary.

Scope: Per-Guild + Capability Snapshot

Every token is bound to a single guild for v1. Multi-guild tokens are reserved for the Operator tier and are not exposed in the dashboard yet — the schema supports them, the UI does not.

At issuance, Arkanis records two things in the token's guild scope row:

  • Guild ID — the only guild this token can act in.
  • Capability snapshot — the issuing user's current capability set in that guild at the moment of issuance. REST tokens may explicitly narrow the snapshot; MCP tokens take the full inheritable set.

The snapshot is the ceiling. The token can never do anything that wasn't in the issuing user's set at issuance, even if the user is granted more capabilities later. That cuts off a privilege-escalation shape where a long-lived token quietly inherits new powers behind the customer's back.

Live Re-check on Every Call

On every request, Arkanis re-resolves the issuing user's current capabilities in the token's guild and computes snapshot ∩ live. That intersection is what the call is allowed to do.

Two staleness shapes get caught for free:

  • Capabilities granted after issuance — not available to the token (snapshot is the ceiling).
  • Capabilities removed after issuance — not available to the token (live state is the floor). If staff revokes a role mid-day, every subsequent call from that token loses the affected capabilities immediately.

A token whose intersection is empty for the targeted guild returns CAPABILITY_DENIED on every write. Reads still work if any read capability survives the intersection.

ℹ️
Note
There is no token cache. The live re-check runs on every call. Revoking a capability or a role takes effect on the next request — typically within milliseconds.

Optional IP Allowlist

Every token may carry a per-token CIDR allowlist that restricts where the token works. A token with no allowlist is usable from any source IP (the default). A token with one or more entries is fail-closed everywhere else: calls from a non-allowlisted IP return TOKEN_IP_NOT_ALLOWED (HTTP 403) without touching the token's last-used timestamp, so a denied call looks identical to a never-issued call on the row state.

When to use it:

  • A token that should only ever run from a known CI/CD egress (GitHub Actions, GitLab CI, CircleCI).
  • A token used from a single jump host, bastion, or internal automation server.
  • A token bound to a fixed office IP block while staff travel uses dashboard access.

Accepted entry shapes:

  • IPv4 CIDR — 10.0.0.0/8, 192.0.2.0/24
  • IPv6 CIDR — 2001:db8::/32
  • Bare IPv4 (treated as /32) — 203.0.113.5
  • Bare IPv6 (treated as /128) — 2001:db8::1

IPv4-mapped IPv6 callers (::ffff:a.b.c.d — emitted by some proxies) are normalised back to IPv4 before the match, so an allowlist with 192.0.2.0/24 matches a caller arriving as ::ffff:192.0.2.10.

The allowlist is editable in place from the dashboard — open Settings → API & MCP → REST tokens, click Edit next to the IPs: row, edit the list, and save. Clearing the list (empty textarea) reverts the token to the any-IP default. The check runs after the cryptographic verify on every call; updates take effect on the next request.

💡
Tip
The allowlist is one of several layers — capabilities, expiry, rotation, and auto-revoke all still apply. Treat it as defense-in-depth for a credential whose primary protection is still the secret itself.

Rotation Cadence (90 / 180 days)

Long-lived credentials accumulate exposure surface. Think CI logs, ex-staff laptops, shells backgrounded months ago. Arkanis nudges you to rotate on a fixed cadence so that exposure has a bounded lifetime, even if you never actively touch the token.

The two thresholds:

  • 90 days since the token was last issued or rotated → the dashboard shows a dismissable soft warning on the row. Click Snooze to suppress the warning until the 180-day threshold catches up.
  • 180 days → a nightly bot job stamps rotation_required_at on the token. The dashboard then shows a non-dismissable banner, and the bot sends a Discord DM to the operator who issued the token. The token continues to work. The cadence is a reminder, not a kill switch.

The 90-day soft warning is computed client-side from the row's created_at or rotated_at timestamp; no server-side flag is set until 180 days. Snoozing the soft warning updates rotation_warning_dismissed_at; it does NOT defer the 180-day non-dismissable banner.

ℹ️
Note
Notification channels: Discord DM + dashboard banner. No email. The Arkanis platform does not collect or store operator email addresses, so a rotation email would require a separate opt-in surface. That is a fair feature build but not a hardening one, and out of scope for this wave. If the DM is suppressed (privacy settings, no shared guild) the banner is still authoritative and remains until you rotate.
💡
Tip
The cadence is informational, not a kill switch. Tokens past 180 days continue to work. Automated revocation of tokens that were never rotated and never had their warning dismissed is deferred to a future wave (270-day window), and will only ship after the 90/180 cadence has been live for at least 90 days so no operator gets a token yanked before they have seen a banner or DM.

Auto-Revoke Triggers

Some role changes are large enough that we revoke the token outright instead of relying on the live re-check to silently empty its intersection. Auto-revoke fires on:

  • Issuing user leaves the guild — immediate revoke.
  • Issuing user loses the capability set the token was scoped against — revoke after a 24-hour grace period to handle transient role changes (e.g. a re-assigned role re-added the same day).
  • Explicit revoke from the dashboard — immediate.

Expiry: 7 / 30 / 90 / None

Customer picks the expiry at issuance. Default is 90 days.

OptionWhen to use
7 daysShort-lived integrations, throwaway scripts, one-off backfills.
30 daysInternal automation you'll touch monthly.
90 daysDefault. The right answer for most integrations.
No expiryLong-lived production scripts where rotation is your discipline, not the platform's.

Past expiry, every call returns TOKEN_EXPIRED. Use renew (keeps the same secret) or rotate (issues a new secret) to extend the token's life.

Lifecycle: Renew, Rotate, Revoke

Three dashboard actions cover the lifecycle. Each is its own audit event.

Renew

Extends expires_at by 90 days (or your chosen length). Same secret. Available on non-expired tokens. Use when the token is still trusted and you just want more runway.

Rotate

Issues a new plaintext secret on the same token row — name, scope, capabilities, and expiry are preserved. The old secret is killed immediately by default. Optionally enable a 5-minute soft-overlap window per rotation so a running script can swap secrets without downtime.

Use rotate when a token might have leaked (log scrape, screenshot, accidental commit) but you want to keep the audit lineage and the integration alive.

Revoke

Immediate kill. Subsequent calls return TOKEN_REVOKED. Use when a token is confirmed compromised, or the integration is done with.

💡
Tip
Renewing keeps any leak alive. If you have any doubt about a token's exposure, rotate instead. Rotation is the security-correct response.

Creating a REST Token

REST tokens are issued from the dashboard. Optimized for developer ergonomics — no device flow, no polling.

1

Open the dashboard panel

Sign in at dashboard.arkanis.gg, pick your guild, open the Settings Panel from the sidebar, and find the Developer Mode card. Toggle it on. API & MCP now appears in your sidebar's Dashboard section — open it and switch to the REST tokens tab.

The panel is gated to the Discord guild owner. Developer Mode is a per-browser preference, so you only need to flip it once per machine.

2

Click Create REST API Token

Fill the modal:

  • Name — what this token is for. Shows up in audit rows.
  • Guild scope — single guild for v1.
  • Capabilities — defaults to your full inheritable set; narrow it down if the integration only needs a subset.
  • Expiry — 7d, 30d, 90d, or never.
3

Copy the plaintext token

On success the dashboard shows the plaintext arkpat_... token with a copy button and a curl example pre-populated with your token. This is your only chance to copy it. Close the modal and the plaintext is gone forever; only the metadata (name, prefix, scope, expiry, last used) remains.

⚠️
Warning
Treat the token like a password. Don't paste it into chat, don't commit it, don't store it in plaintext config that gets backed up to a shared drive. Use your platform's secret store.

Creating an MCP Token (Device Flow)

MCP tokens use an OAuth device flow because the consumer is a local CLI process, not a script you control. The CLI never sees your dashboard session, and the dashboard never sees your file system.

1

Install the MCP server

npm install -g @arkanis/mcp-server or npx @arkanis/mcp-server pair for a one-shot pairing.

2

Run arkanis-mcp pair

The CLI calls POST /oauth/device/code, prints an 8-character user code (e.g. WGNX-7K3F), and shows the verification URL. The auto-detected client name (Claude Desktop, Claude Code, or generic) appears in the dashboard for context.

3

Approve in the dashboard

Open the URL the CLI printed (or, with Developer Mode enabled, go to API & MCP → MCP tokens in the sidebar and click Approve pairing). Sign in if needed, enter the 8-character code, review the requested scope (guild + capabilities inherited from your user), and click Authorize.

4

CLI receives the token and prints client config

The CLI was polling POST /oauth/device/token in the background. On approval it receives the plaintext token, writes it to~/.config/arkanis-mcp/config.json (or the platform equivalent), and prints a Claude Desktop / Claude Code config snippet to stdout for you to paste into your MCP client config.

ℹ️
Note
The MCP server does not write to your Claude client's config file directly — that would cross a trust boundary you should own. The CLI prints the snippet; you paste it.

Destructive Actions Need a Second Step

Capability checks govern what a token can do. Destructive operations (bans, strikes, long mutes, role wipes, panel deletes) require an additional re-auth window and a sentinel confirmation string.

  • Re-auth window — 15 minutes (configurable). Opened from the dashboard with Approve next destructive action. One window covers every destructive call on that token until it expires or you close it.
  • Sentinel confirmation — every destructive call also requires a _confirmation field matching a deterministic template (e.g. BAN USER 12345 IN GUILD 67890 PERMANENT). Prevents copy-paste accidents and prompt-injection misuse via MCP.

Full details, the complete list of destructive operations, and the sentinel templates live on the Destructive Actions page.

Auth-Related Error Codes

The REST API and MCP tool envelopes return these auth-related codes. Full error reference on the Rate Limits & Errors page.

CodeMeaning
TOKEN_INVALIDToken doesn't exist or the secret doesn't match.
TOKEN_EXPIREDToken is past its expires_at. Renew or rotate.
TOKEN_REVOKEDToken was revoked manually or auto-revoked. Create a new one.
CAPABILITY_DENIEDThe live intersection lacks the capability this call needs.
RE_AUTH_REQUIREDDestructive call outside the re-auth window. details.reauth_url points to the approval page.