Quick Start

Your first successful API call, end to end. The coding takes a few minutes. Registration is reviewed by a person, so plan for approval to arrive within 24 hours before you can complete step 2.

Three things that trip people up

  • Your app must be approved before the flow works at all. An unapproved client_id fails at the authorize step with invalid_client.
  • redirect_uri is matched exactly. No wildcards, no trailing-slash forgiveness. It must be one of the URIs you registered.
  • You can only request scopes your app was approved for. Asking for more returns invalid_scope, delivered to your redirect URI rather than as a direct error.
1

Register your application

Visit the registration page and fill in your app details. You will need a Spellweave account first. You will be asked to accept the Developer Terms of Service, which carry the attribution requirement in step 6.

Every self-registered app is a confidential client: it authenticates at the token endpoint with a secret, so it must run on a server you control. If your app cannot keep a secret, say so in your intended use and we will review it.

Getting your credentials

Credentials are not emailed to you, and they do not exist until you ask for them. Once your app is approved, open it from My Apps in your Spellweave account and press Generate credentials.

The client_secret is displayed once, at that moment. It is stored only as a hash, so nobody can show it to you again. Put it straight into your secret store. If you lose it, or suspect it has leaked, use Roll Secret on the same page to issue a new one. Rolling invalidates the old secret immediately.

2

Redirect users to authorize

Start the OAuth flow by redirecting users to the authorization endpoint. PKCE is mandatory, and code_challenge_method must be S256. The plain method is rejected.

JavaScript
// Generate a random code_verifier (43-128 chars, URL-safe).
// Keep it for step 3: the same value must come back.
const codeVerifier = generateRandomString(64);

// Hash it with SHA-256 and base64url-encode
const codeChallenge = base64url(sha256(codeVerifier));

const authUrl = new URL("https://public-api.spellweave.app/api/v1/oauth/authorize");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", "YOUR_CLIENT_ID");
authUrl.searchParams.set("redirect_uri", "https://yourapp.com/callback"); // exact match
authUrl.searchParams.set("scope", "profile:read collections:read decks:read");
authUrl.searchParams.set("state", generateRandomString(32));
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");

window.location.href = authUrl.toString();

Available scopes are profile:read, collections:read, collections:write, decks:read and decks:write. profile:read is always granted. Ask only for what you use: the consent screen shows every scope to the user, and a long list costs you approvals. See Scopes for what each one covers.

Always send state and check it on the way back. Errors after this point are delivered to your redirect_uri as error and error_description query parameters, so your callback needs to handle a failure as well as a code.

3

Exchange the code for tokens

The user is redirected back to your redirect_uri with an authorization code. It is single use and expires 10 minutes after it is issued, so exchange it as soon as it arrives.

Exchange the code
curl -X POST https://public-api.spellweave.app/api/v1/oauth/token \
  -d "grant_type=authorization_code" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "code=AUTHORIZATION_CODE" \
  -d "redirect_uri=https://yourapp.com/callback" \
  -d "code_verifier=YOUR_CODE_VERIFIER"
Token response
{
  "access_token": "kZ8Qw3nR7tYb1cVx0pLmJ4hG6dSaF2eN9uT5iO8yA3s",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "9pXmL2vB6nQ4wR8tYuI0oP3aS5dF7gH1jK4lZ6xC8vB",
  "scope": "profile:read collections:read decks:read"
}

Tokens are opaque random strings, not JWTs. There is nothing to decode and no claims inside. Treat them as unguessable identifiers, store them encrypted, and call /api/v1/me if you need to know who the token belongs to. Their length is not contractual, so size your column generously rather than assuming today's value.

A wrong or missing client_secret returns 401 invalid_client. A bad code, a mismatched code_verifier or a mismatched redirect_uri returns 400 invalid_grant. The difference tells you which half to debug.

4

Make API calls

Use the access token to call any endpoint within the granted scopes.

Request
curl https://public-api.spellweave.app/api/v1/decks \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response
{
  "data": [
    {
      "id": "7bcf56bb-894b-46f3-8dff-d7df6681d328",
      "name": "Gruul Interactive Beats",
      "format": "commander",
      "commander_name": "Chishiro, the Shattered Blade",
      "engine_version": "5.08",
      "target_bracket": 2,
      "bracket": {
        "target": 2,
        "actual": 2,
        "status": "ok",
        "checked_at": "2026-08-21T18:33:55Z",
        "basis": "compliance",
        "measures": "Game changer count and illegal or banned combo count. Not a power rating.",
        "max_detectable": 4
      },
      "origin": "imported",
      "import_source": "moxfield",
      "build": null,
      "created_at": "2026-08-21T18:33:52Z"
    }
  ],
  "meta": {
    "request_id": "cb115cbb-2183-4da5-9d49-dc629447d1c1",
    "timestamp": "2026-09-06T17:26:22Z",
    "has_more": false,
    "next_cursor": null
  }
}

Rate limits

New apps get 60 requests per minute and 10,000 per day. Limits are set per app, so yours can be raised. Read them from the headers on every response rather than hardcoding these numbers or counting requests yourself. The headers are exposed to browsers via CORS.

Response headers
ratelimit-limit: 60
ratelimit-remaining: 59
ratelimit-reset: 60
ratelimit-policy: 60;w=60, 10000;w=86400

Exceeding a limit returns 429 with a Retry-After header, and the same number as retry_after in the body. Wait for it rather than retrying immediately. Paginated endpoints return meta.next_cursor: follow it until has_more is false, and never assume the page size you asked for is the page size you got.

5

Handle token refresh

Access tokens expire after 1 hour. Refresh tokens last 90 days. Use the refresh token to get a new pair without re-prompting the user.

Refresh tokens
curl -X POST https://public-api.spellweave.app/api/v1/oauth/token \
  -d "grant_type=refresh_token" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "refresh_token=YOUR_REFRESH_TOKEN"

Each refresh token can be used once, and every exchange returns a new one. Persist the new token before you use it, in the same write as anything else that depends on it.

Reuse is treated as a compromise

If a refresh token that has already been used is presented again, we assume it leaked and revoke the whole token family, including the user's current access tokens. That user must authorize your app again.

This is a deliberate protection, and it is also the most common way a working integration breaks: a blind retry after a timeout replays the old token, and every session for that user drops. Make refresh a single-flight operation per user, and on a network error read your stored token back before retrying rather than resending the one you had in hand.

6

Attribute the data you display

If you show Spellweave-derived analysis in your product, such as brackets, combos, deck statistics or Deck Lab output, you must disclose that it comes from spellweave.app. This is a requirement of the Developer Terms you accepted at registration, not a courtesy.

Responses carrying computed deck data include the attribution alongside the data, so you never have to look it up:

Attribution in meta
"meta": {
  "attribution": {
    "text": "Deck analysis by Spellweave",
    "url": "https://spellweave.app",
    "required": true,
    "notice": "If you display this data in your product, you must disclose that it comes from spellweave.app. See the Spellweave Developer Terms of Service."
  }
}

Rendering the text as a link to url near the data satisfies it. Raw data the user already owns, such as their own deck and collection lists, does not carry the block and does not need the notice.

Where to go next