🔌 Developer guide

Build with the Flashcards.gg API.

A clean REST API with an OpenAPI 3 spec — manage decks, cards, and shares from any HTTP client, then wire it into ChatGPT GPTs, Claude tool use, Gemini function calling, or your own scripts.

Interactive API reference Get an API key
Contents

Quick start

Three steps to your first authenticated call:

  1. Sign in at flashcards.gg/account with the same Google account you use in the app.
  2. Click Manage API keys, generate a key, copy the flc_… value (shown once).
  3. Set it on every request as Authorization: Bearer flc_….
$ curl -H "Authorization: Bearer flc_..." \
       https://flashcards.gg/api/v1/sets
{
  "data": [
    {
      "id": "set_65a3b7c9d8f4e2a1b3c5d7e9",
      "name": "Spanish",
      "cardCount": 47,
      "updatedAt": "2026-06-15T16:30:00.000Z",
      "frontLang": "es",
      "backLang": "en",
      "studySide": "front"
    }
  ],
  "cursor": null,
  "hasMore": false
}

Create a deck with two cards in one call:

$ curl -X POST -H "Authorization: Bearer flc_..." \
       -H "Content-Type: application/json" \
       -d '{
         "name": "Greek alphabet",
         "cards": [
           {"key": "α", "value": "alpha"},
           {"key": "β", "value": "beta"}
         ]
       }' \
       https://flashcards.gg/api/v1/sets

Add a single card to an existing deck:

$ curl -X POST -H "Authorization: Bearer flc_..." \
       -H "Content-Type: application/json" \
       -d '{"key": "γ", "value": "gamma"}' \
       https://flashcards.gg/api/v1/sets/set_65a3.../cards

Authentication

Every request needs Authorization: Bearer <token>. The token can be either of:

API keys are scoped to the user who created them — they can read and write that user's decks, nothing else. The same endpoint serves both the app and external clients without code branching.

The flc_ prefix is a secret-scanner signal. GitHub Advanced Security, gitleaks, and trufflehog detect leaked keys and may auto-revoke them. Never commit a raw key to source control.

Rate limits

100 requests per minute per key. Two keys for the same user get independent budgets, so you can give a noisy bot its own key without throttling your main client.

Every response carries:

X-RateLimit-Limit:     100
X-RateLimit-Remaining: 87
X-RateLimit-Reset:     1781532840  # unix seconds

When you hit the cap, the next request gets 429 with a Retry-After header:

HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Remaining: 0

{
  "error": "rate_limited",
  "message": "Too many requests. Limit: 100/min.",
  "retryAfterSeconds": 23
}

ChatGPT — custom GPT

Build a custom GPT that adds, edits, and lists flashcards while you chat. Requires a ChatGPT Plus or Team account.

  1. Open chat.openai.com/gpts/editor and create a new GPT.
  2. In Configure → Actions, click Create new action.
  3. Under Schema, click Import from URL and paste:
    https://flashcards.gg/api/v1/openapi.json
  4. Under Authentication, choose API KeyAuth Type: Bearer, and paste your flc_… key.
  5. Set the GPT instructions to the prompt from the recommended system prompt section.
  6. Save and publish. Test by asking it to list, create, or update a deck.
Auth scope. The key you paste into the GPT is shared by anyone using it (if you publish publicly). For a personal GPT, use your own key. For a multi-user GPT, consider an OAuth flow instead — out of scope for the v1 API but coming later.

Claude — tool use (Anthropic API)

Claude.ai consumer doesn't yet support custom GPT-style actions, but the Anthropic API supports tool use — you can wire our OpenAPI spec into tools manually:

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();
const FLC_KEY = process.env.FLC_KEY;

const tools = [
  {
    name: 'listSets',
    description: 'List the user\'s flashcard sets.',
    input_schema: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'Optional exact-match filter by set name.' },
      },
    },
  },
  {
    name: 'addCard',
    description: 'Add a single card to a deck the user owns.',
    input_schema: {
      type: 'object',
      required: ['setId', 'key', 'value'],
      properties: {
        setId: { type: 'string', description: 'set_<24hex> from listSets.' },
        key:   { type: 'string', description: 'Front side text.' },
        value: { type: 'string', description: 'Back side text.' },
      },
    },
  },
];

async function callTool(name, input) {
  const base = 'https://flashcards.gg/api/v1';
  const auth = { 'Authorization': 'Bearer ' + FLC_KEY };
  switch (name) {
    case 'listSets': {
      const url = input.name
        ? `${base}/sets?name=${encodeURIComponent(input.name)}`
        : `${base}/sets`;
      const r = await fetch(url, { headers: auth });
      return await r.json();
    }
    case 'addCard': {
      const r = await fetch(`${base}/sets/${input.setId}/cards`, {
        method: 'POST',
        headers: { ...auth, 'Content-Type': 'application/json' },
        body: JSON.stringify({ key: input.key, value: input.value }),
      });
      return await r.json();
    }
  }
}

// ... feed tool_use blocks back to client.messages.create()
      

Full reference: see Anthropic's tool use guide.

Gemini — function calling

Gemini's function-calling API consumes the same shape we expose in OpenAPI:

from google import genai
from google.genai import types
import requests, os

FLC_KEY = os.environ['FLC_KEY']

list_sets_fn = types.FunctionDeclaration(
    name='listSets',
    description='List the user\'s flashcard sets.',
    parameters={
        'type': 'OBJECT',
        'properties': {
            'name': {'type': 'STRING', 'description': 'Optional exact-match filter.'},
        },
    },
)
add_card_fn = types.FunctionDeclaration(
    name='addCard',
    description='Add a single card to a deck.',
    parameters={
        'type': 'OBJECT',
        'required': ['setId', 'key', 'value'],
        'properties': {
            'setId': {'type': 'STRING'},
            'key':   {'type': 'STRING'},
            'value': {'type': 'STRING'},
        },
    },
)

tools = types.Tool(function_declarations=[list_sets_fn, add_card_fn])
client = genai.Client()

def call_tool(name, args):
    headers = {'Authorization': f'Bearer {FLC_KEY}'}
    if name == 'listSets':
        url = 'https://flashcards.gg/api/v1/sets'
        if 'name' in args:
            url += f'?name={args["name"]}'
        return requests.get(url, headers=headers).json()
    if name == 'addCard':
        return requests.post(
            f'https://flashcards.gg/api/v1/sets/{args["setId"]}/cards',
            headers={**headers, 'Content-Type': 'application/json'},
            json={'key': args['key'], 'value': args['value']},
        ).json()

Recommended system prompt

Paste this (or your translated version) into the GPT instructions / Claude system / Gemini system. It teaches the model the LLM-friendly bits of the API:

You help the user manage their Flashcards.gg flashcards via the public REST
API. Use the available tools to list, create, update, share, and delete
decks and cards on their behalf.

Key conventions:
- Resource IDs are prefixed: sets are `set_<24hex>`, cards are `card_<...>`.
- When the user references a deck by name (e.g. "my Spanish deck"), call
  listSets with the name filter to resolve to an ID before acting.
- Adding a single card to an existing deck → POST /sets/{id}/cards.
  Don't replace the whole deck for one card.
- PATCH only the fields you're changing; PUT replaces the whole set.
- After mutations, briefly tell the user what changed and where (deck
  name, card count, link if applicable).
- If you hit `rate_limited` (429), tell the user to wait the
  retryAfterSeconds shown in the response.
- For destructive operations (deleteSet, revokeShare), ask the user to
  confirm before executing.

Avoid asking for the API key — the host already has it bound.

Resource shape & concurrency

Set shape

{
  "id": "set_65a3b7c9d8f4e2a1b3c5d7e9",
  "name": "Spanish",
  "cards": [
    { "id": "card_88f4...", "key": "hola", "value": "hello" }
  ],
  "updatedAt": "2026-06-15T16:30:00.000Z",
  "frontLang": "es",
  "backLang": "en",
  "studySide": "front"
}

Optimistic locking (ETag)

GET responses include ETag: W/"<updatedAt-millis>". Pass it back as If-Match on PUT / PATCH / DELETE:

$ curl -X PUT -H "Authorization: Bearer flc_..." \
       -H 'If-Match: W/"1700000000000"' \
       -d '{"name":"Renamed","cards":[]}' \
       https://flashcards.gg/api/v1/sets/set_...

On stale ETag you get 412 with the current value:

HTTP/1.1 412 Precondition Failed
{
  "error": "stale_etag",
  "message": "set was modified by another client",
  "currentEtag": "W/\"1700001234567\""
}

Cursor pagination

GET /sets?limit=50 returns a cursor field — opaque base64. Pass it back as ?cursor=… for the next page. hasMore: false means you're done.

Error codes

All errors return a JSON body of the shape:

{ "error": "<machine_readable_code>", "message": "<human readable>", "field": "<optional>" }

Need help?

Email flashcards.gg@gmail.com with the request id, timestamp, and what you expected to happen. The spec at /api/v1/openapi.json is the source of truth — if anything on this page contradicts it, the spec wins.