Quick start
Three steps to your first authenticated call:
- Sign in at flashcards.gg/account with the same Google account you use in the app.
- Click Manage API keys, generate a key, copy the
flc_…value (shown once). - 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:
- A personal API key from /account/api-keys, prefixed
flc_. - A Firebase ID token from a signed-in user (used by the Flutter app).
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.
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.
- Open chat.openai.com/gpts/editor and create a new GPT.
- In Configure → Actions, click Create new action.
- Under Schema, click Import from URL and paste:
https://flashcards.gg/api/v1/openapi.json - Under Authentication, choose API Key →
Auth Type: Bearer, and paste your
flc_…key. - Set the GPT instructions to the prompt from the recommended system prompt section.
- Save and publish. Test by asking it to list, create, or update a deck.
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>" }
401 auth_required— missing or invalid Authorization header.400 invalid_id— id doesn't matchset_<24hex>.400 invalid_name/invalid_card_field/invalid_study_side/invalid_lang— field validation.400 empty_patch— PATCH body had no fields.404 set_not_found— set doesn't exist or caller doesn't own it.404 card_not_found— set exists but card id is unknown.404 share_not_found— token doesn't exist or was revoked.409 name_taken— caller already has a set with that name.412 stale_etag— concurrent write; body includescurrentEtag.429 rate_limited— per-key cap exceeded; checkRetry-After.
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.