Cardagain API

v1.0.0 · OpenAPI 3.0.3 · openapi.json

The HTTP API behind cardaga.in — decks, notes, cards, the study queue, review history, import and export. Authenticate with an API key created on your account page and sent as `Authorization: Bearer cardi_…`. **Pagination is keyset, never offset.** List endpoints return a `nextCursor`; pass it back as `?cursor=`. There is no `?offset=` on any endpoint, and sending one is a 400 rather than a silently wrong page. **Errors share one envelope** — `{ error: { code, message, path?, details?, requestId } }`. Quote `requestId` when reporting a problem. **Another user’s resource answers 404, never 403.** A 403 would confirm the row exists.
POST

POST /auth/device-tokens

Exchange a Google or Apple ID token, or a session, for a device token.

Two entry modes. With `idToken`, no prior credential is needed -- the token itself is verified in-process, by the provider `idToken.provider` names (`google` when omitted), the same way a browser completing that provider's redirect would be. Without it, the caller must already hold a session cookie. Either way the minted token is scoped to `sync` on the `collection` surface and expires in 180 days.

Request body

Request body
FieldTypeRequiredNotes
namestringrequired1–120 characters
idTokenobjectoptional
handoffobjectoptional

Request

curl -X POST "https://api.cardaga.in/auth/device-tokens" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
tokenstringrequiredat least 1 characters
expiresAtdate-timerequired
userIduuidrequired
DELETE

DELETE /auth/device-tokens/current

Revoke the device token that made this call.

The bearer token the request carries is revoked and answers 401 from then on. Other device tokens of the same account and any browser session are untouched.

Request

curl -X DELETE "https://api.cardaga.in/auth/device-tokens/current" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
revokedbooleanrequiredone of: true
POST

POST /auth/device-handoffs

Mint a one-time code that hands this browser session to an app.

Requires a browser session (a device token cannot mint one). The code is single-use, expires in five minutes, and is exchanged at `POST /auth/device-tokens` with `handoff` and the PKCE verifier whose S256 challenge this request carried.

Request body

Request body
FieldTypeRequiredNotes
codeChallengestringrequired

Request

curl -X POST "https://api.cardaga.in/auth/device-handoffs" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
codestringrequiredat least 1 characters
expiresAtdate-timerequired
POST

POST /auth/apple/notifications

Receive Apple's notice that a Sign in with Apple account changed.

Called by Apple, not by a client. `payload` is a JWS Apple signed; it is refused unless its issuer is Apple, its audience is this deployment's app, and its signature verifies against Apple's published keys. A verified `consent-revoked` or `account-deleted` removes the Apple link from the account and ends every session and device token it holds; the account and its collection stay. Forwarding changes are acknowledged and change nothing. 404 on a deployment with no Sign in with Apple configured.

Request body

Request body
FieldTypeRequiredNotes
payloadstringrequiredat least 1 characters

Request

curl -X POST "https://api.cardaga.in/auth/apple/notifications" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
receivedbooleanrequiredone of: true
GET

GET /decks

List decks, flat and paginated.

A flat, keyset-paginated scan, for pickers and autocomplete. Pass the `nextCursor` from one page as `?cursor=` for the next; there is no offset, and sending one is a 400. Use `tree` instead when you need nesting or due counts — this route returns neither.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200
parentIduuidoptional
qstringoptionalat most 500 characters
includeFilteredunknownoptional

Request

curl "https://api.cardaga.in/decks" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
POST

POST /decks

Create a deck, optionally under a parent.

Pass `parentId` to nest. The response carries the materialized `path` and the `::`-joined `displayPath`, both derived server-side — do not construct either yourself. A sibling with the same name under the same parent is a 409.

Request body

Request body
FieldTypeRequiredNotes
namestringrequired1–200 characters
parentIduuid | nulloptional
descriptionstringoptionalat most 10000 characters
optionsPresetIduuid | nulloptional
isFilteredbooleanoptional
filterConfigobjectoptional

Request

curl -X POST "https://api.cardaga.in/decks" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
iduuidrequired
parentIduuid | nullrequired
namestringrequired
pathstringrequired
displayPathstringrequired
optionsPresetIduuid | nullrequired
descriptionstringrequired
isCollapsedbooleanrequired
isFilteredbooleanrequired
filterConfigobject | nullrequired
newLimitinteger | nulloptional>= 0
reviewLimitinteger | nulloptional>= 0
newLimitTodayobject | nulloptional
reviewLimitTodayobject | nulloptional
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
GET

GET /decks/tree

The whole deck tree with due counts, as the deck screen renders it.

One bounded read of the entire collection, with new/learning/review counts rolled up to each node the way the deck screen shows them. Unpaginated by design: a deck tree is small and a partial one is not useful. Counts honour the caller’s day rollover, so this route’s answer changes at their rollover hour rather than at midnight UTC.

Request

curl "https://api.cardaga.in/decks/tree" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
nodesunknown[]required
GET

GET /decks/:id

Fetch one deck.

The deck itself, with no counts and no children. Another user’s deck answers 404, identically to a deck that does not exist — the two are deliberately indistinguishable.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/decks/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
parentIduuid | nullrequired
namestringrequired
pathstringrequired
displayPathstringrequired
optionsPresetIduuid | nullrequired
descriptionstringrequired
isCollapsedbooleanrequired
isFilteredbooleanrequired
filterConfigobject | nullrequired
newLimitinteger | nulloptional>= 0
reviewLimitinteger | nulloptional>= 0
newLimitTodayobject | nulloptional
reviewLimitTodayobject | nulloptional
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
PATCH

PATCH /decks/:id

Rename a deck, or change its options.

A rename is the expensive half: it rewrites `path` and `displayPath` for every descendant in one statement, so renaming a deck with a large subtree costs proportionally more than changing its preset. Cosmetic changes take neither that cost nor that lock, which is why the two are dispatched separately. A name colliding with a sibling is a 409.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
namestringoptional1–200 characters
descriptionstringoptionalat most 10000 characters
optionsPresetIduuid | nulloptional
isCollapsedbooleanoptional
filterConfigobject | nulloptional
newLimitinteger | nulloptional>= 0
reviewLimitinteger | nulloptional>= 0
newLimitTodayobject | nulloptional
reviewLimitTodayobject | nulloptional

Request

curl -X PATCH "https://api.cardaga.in/decks/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
parentIduuid | nullrequired
namestringrequired
pathstringrequired
displayPathstringrequired
optionsPresetIduuid | nullrequired
descriptionstringrequired
isCollapsedbooleanrequired
isFilteredbooleanrequired
filterConfigobject | nullrequired
newLimitinteger | nulloptional>= 0
reviewLimitinteger | nulloptional>= 0
newLimitTodayobject | nulloptional
reviewLimitTodayobject | nulloptional
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
DELETE

DELETE /decks/:id

Delete a deck, moving or cascading its cards explicitly.

The body must state where the cards go, because there is no safe default: `ON DELETE RESTRICT` in the database refuses an unqualified delete, and this route cannot express one. `affected` reports how many cards were moved or deleted.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl -X DELETE "https://api.cardaga.in/decks/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
POST

POST /decks/:id/reposition

Re-parent a deck, rewriting the subtree path.

Moves the deck and everything beneath it. Re-parenting a deck into its own descendant is a 409 rather than a cycle. Scheduling is untouched: cards keep their state, and their preset unless it was inherited, in which case they inherit the new parent’s.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
newParentIduuid | nullrequired
requestIdstringoptional8–64 characters

Request

curl -X POST "https://api.cardaga.in/decks/{id}/reposition" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
parentIduuid | nullrequired
namestringrequired
pathstringrequired
displayPathstringrequired
optionsPresetIduuid | nullrequired
descriptionstringrequired
isCollapsedbooleanrequired
isFilteredbooleanrequired
filterConfigobject | nullrequired
newLimitinteger | nulloptional>= 0
reviewLimitinteger | nulloptional>= 0
newLimitTodayobject | nulloptional
reviewLimitTodayobject | nulloptional
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
GET

GET /deck-presets

List option presets.

Keyset-paginated. A preset is a named group of scheduling options — new/day, maximum interval, learning steps, burying — shared by however many decks point at it.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200

Request

curl "https://api.cardaga.in/deck-presets" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
POST

POST /deck-presets

Create an option preset.

Options you omit take the same defaults a new collection starts with, so a preset that sets only `newPerDay` is complete rather than partial. Creating one changes no deck until a deck points at it.

Request body

Request body
FieldTypeRequiredNotes
namestringrequired1–200 characters
newPerDayintegeroptional>= 0, <= 9999
reviewsPerDayintegeroptional>= 0, <= 99999
learningStepsMinsnumber[]optional
relearningStepsMinsnumber[]optional
graduatingIntervalDaysintegeroptional>= 1
easyIntervalDaysintegeroptional>= 1
startingEasenumberoptional>= 1.3, <= 5
maxIntervalDaysintegeroptional>= 1, <= 36500
leechThresholdintegeroptional>= 1, <= 99
capAnswerTimeSecsintegeroptional>= 1, <= 9999
leechActionstringoptionalone of: suspend | tag_only
buryNewSiblingsbooleanoptional
buryReviewSiblingsbooleanoptional
newCardGatherOrderstringoptionalone of: due | random | position
reviewSortOrderstringoptionalone of: due_date_random | due_date_deck | ascending_intervals | descending_intervals | relative_overdueness
newMixstringoptionalone of: mix_with_reviews | after_reviews | before_reviews
interdayLearningMixstringoptionalone of: mix_with_reviews | after_reviews | before_reviews
showTimerbooleanoptional
stopTimerOnAnswerbooleanoptional
schedulerstringoptionalone of: fsrs | sm2
desiredRetentionnumberoptional>= 0.7, <= 0.99
hardMultipliernumberoptional>= 0.5, <= 1.3
easyMultipliernumberoptional>= 1, <= 5
intervalMultipliernumberoptional>= 0.5, <= 2
lapseMultipliernumberoptional>= 0, <= 1
minimumLapseIntervalDaysintegeroptional>= 1, <= 36500
fsrsWeightsnumber[]optional
easyDaysPercentagesnumber[]optional

Request

curl -X POST "https://api.cardaga.in/deck-presets" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
iduuidrequired
namestringrequired
newPerDayintegerrequired
reviewsPerDayintegerrequired
learningStepsMinsnumber[]required
relearningStepsMinsnumber[]required
graduatingIntervalDaysintegerrequired
easyIntervalDaysintegerrequired
startingEasenumberrequired
maxIntervalDaysintegerrequired
leechThresholdintegerrequired
capAnswerTimeSecsintegerrequired
leechActionstringrequiredone of: suspend | tag_only
buryNewSiblingsbooleanrequired
buryReviewSiblingsbooleanrequired
newCardGatherOrderstringrequiredone of: due | random | position
reviewSortOrderstringrequiredone of: due_date_random | due_date_deck | ascending_intervals | descending_intervals | relative_overdueness
newMixstringrequiredone of: mix_with_reviews | after_reviews | before_reviews
interdayLearningMixstringrequiredone of: mix_with_reviews | after_reviews | before_reviews
showTimerbooleanrequired
stopTimerOnAnswerbooleanrequired
schedulerstringrequiredone of: fsrs | sm2
desiredRetentionnumberrequired
hardMultipliernumberrequired
easyMultipliernumberrequired
intervalMultipliernumberrequired
lapseMultipliernumberrequired
minimumLapseIntervalDaysintegerrequired
fsrsWeightsnumber[]required
easyDaysPercentagesnumber[]required
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
GET

GET /deck-presets/:id

Fetch one option preset.

The preset’s full option set. It does not report which decks use it; list decks and filter on `presetId` for that.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/deck-presets/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
namestringrequired
newPerDayintegerrequired
reviewsPerDayintegerrequired
learningStepsMinsnumber[]required
relearningStepsMinsnumber[]required
graduatingIntervalDaysintegerrequired
easyIntervalDaysintegerrequired
startingEasenumberrequired
maxIntervalDaysintegerrequired
leechThresholdintegerrequired
capAnswerTimeSecsintegerrequired
leechActionstringrequiredone of: suspend | tag_only
buryNewSiblingsbooleanrequired
buryReviewSiblingsbooleanrequired
newCardGatherOrderstringrequiredone of: due | random | position
reviewSortOrderstringrequiredone of: due_date_random | due_date_deck | ascending_intervals | descending_intervals | relative_overdueness
newMixstringrequiredone of: mix_with_reviews | after_reviews | before_reviews
interdayLearningMixstringrequiredone of: mix_with_reviews | after_reviews | before_reviews
showTimerbooleanrequired
stopTimerOnAnswerbooleanrequired
schedulerstringrequiredone of: fsrs | sm2
desiredRetentionnumberrequired
hardMultipliernumberrequired
easyMultipliernumberrequired
intervalMultipliernumberrequired
lapseMultipliernumberrequired
minimumLapseIntervalDaysintegerrequired
fsrsWeightsnumber[]required
easyDaysPercentagesnumber[]required
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
PATCH

PATCH /deck-presets/:id

Change an option preset.

Takes effect for **every** deck that points at this preset, on the next queue build — that shared blast radius is the whole reason presets are a separate resource rather than fields on a deck. Cards already in a learning step keep the steps they started.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
namestringoptional1–200 characters
newPerDayintegeroptional>= 0, <= 9999
reviewsPerDayintegeroptional>= 0, <= 99999
learningStepsMinsnumber[]optional
relearningStepsMinsnumber[]optional
graduatingIntervalDaysintegeroptional>= 1
easyIntervalDaysintegeroptional>= 1
startingEasenumberoptional>= 1.3, <= 5
maxIntervalDaysintegeroptional>= 1, <= 36500
leechThresholdintegeroptional>= 1, <= 99
capAnswerTimeSecsintegeroptional>= 1, <= 9999
leechActionstringoptionalone of: suspend | tag_only
buryNewSiblingsbooleanoptional
buryReviewSiblingsbooleanoptional
newCardGatherOrderstringoptionalone of: due | random | position
reviewSortOrderstringoptionalone of: due_date_random | due_date_deck | ascending_intervals | descending_intervals | relative_overdueness
newMixstringoptionalone of: mix_with_reviews | after_reviews | before_reviews
interdayLearningMixstringoptionalone of: mix_with_reviews | after_reviews | before_reviews
showTimerbooleanoptional
stopTimerOnAnswerbooleanoptional
schedulerstringoptionalone of: fsrs | sm2
desiredRetentionnumberoptional>= 0.7, <= 0.99
hardMultipliernumberoptional>= 0.5, <= 1.3
easyMultipliernumberoptional>= 1, <= 5
intervalMultipliernumberoptional>= 0.5, <= 2
lapseMultipliernumberoptional>= 0, <= 1
minimumLapseIntervalDaysintegeroptional>= 1, <= 36500
fsrsWeightsnumber[]optional
easyDaysPercentagesnumber[]optional

Request

curl -X PATCH "https://api.cardaga.in/deck-presets/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
namestringrequired
newPerDayintegerrequired
reviewsPerDayintegerrequired
learningStepsMinsnumber[]required
relearningStepsMinsnumber[]required
graduatingIntervalDaysintegerrequired
easyIntervalDaysintegerrequired
startingEasenumberrequired
maxIntervalDaysintegerrequired
leechThresholdintegerrequired
capAnswerTimeSecsintegerrequired
leechActionstringrequiredone of: suspend | tag_only
buryNewSiblingsbooleanrequired
buryReviewSiblingsbooleanrequired
newCardGatherOrderstringrequiredone of: due | random | position
reviewSortOrderstringrequiredone of: due_date_random | due_date_deck | ascending_intervals | descending_intervals | relative_overdueness
newMixstringrequiredone of: mix_with_reviews | after_reviews | before_reviews
interdayLearningMixstringrequiredone of: mix_with_reviews | after_reviews | before_reviews
showTimerbooleanrequired
stopTimerOnAnswerbooleanrequired
schedulerstringrequiredone of: fsrs | sm2
desiredRetentionnumberrequired
hardMultipliernumberrequired
easyMultipliernumberrequired
intervalMultipliernumberrequired
lapseMultipliernumberrequired
minimumLapseIntervalDaysintegerrequired
fsrsWeightsnumber[]required
easyDaysPercentagesnumber[]required
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
DELETE

DELETE /deck-presets/:id

Delete an option preset.

Decks referencing it are not deleted and not blocked: `ON DELETE SET NULL` drops them back to inheriting their parent’s preset, which is why this is a 200 rather than the 409 a restrictive constraint would produce.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl -X DELETE "https://api.cardaga.in/deck-presets/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
okbooleanrequiredone of: true
GET

GET /note-types

List note types with their fields and templates.

A note type is the schema for a note: its ordered fields, and the card templates that turn those fields into cards. Small and bounded, so this returns the whole structure rather than making a caller fetch each one.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200
qstringoptionalat most 200 characters

Request

curl "https://api.cardaga.in/note-types" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
POST

POST /note-types

Create a note type.

Standard types generate one card per template; a cloze type generates one card per distinct cloze ordinal found in its fields. That choice is fixed at creation because the two kinds generate cards by different rules.

Request body

Request body
FieldTypeRequiredNotes
namestringrequired1–200 characters
kindstringoptionalone of: standard | cloze
cssstringoptionalat most 200000 characters
latexPreamblestringoptionalat most 20000 characters
latexPostamblestringoptionalat most 20000 characters
sortFieldOrdinalintegeroptional>= 0
fieldsobject[]required
templatesobject[]required

Request

curl -X POST "https://api.cardaga.in/note-types" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
iduuidrequired
namestringrequired
kindstringrequiredone of: standard | cloze
cssstringrequired
latexPreamblestringrequired
latexPostamblestringrequired
aiNotesstringrequired
sortFieldOrdinalintegerrequired>= 0
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
fieldsobject[]required
templatesobject[]required
GET

GET /note-types/:id

Fetch one note type.

Fields in their display order with their ids, plus every template’s question and answer format. The ids are what `POST /notes` expects as field keys.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/note-types/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
namestringrequired
kindstringrequiredone of: standard | cloze
cssstringrequired
latexPreamblestringrequired
latexPostamblestringrequired
aiNotesstringrequired
sortFieldOrdinalintegerrequired>= 0
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
fieldsobject[]required
templatesobject[]required
PATCH

PATCH /note-types/:id

Rename a note type, or edit its fields and templates.

Editing a template re-renders every card that uses it, which is cheap. Adding or removing a FIELD is not: it rewrites every note of this type. To restructure fields across existing notes, use `migrationPreview` and `migrate`, which are transactional and undoable — this route is not the place for it.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
namestringoptional1–200 characters
cssstringoptionalat most 200000 characters
latexPreamblestringoptionalat most 20000 characters
latexPostamblestringoptionalat most 20000 characters
sortFieldOrdinalintegeroptional>= 0

Request

curl -X PATCH "https://api.cardaga.in/note-types/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
namestringrequired
kindstringrequiredone of: standard | cloze
cssstringrequired
latexPreamblestringrequired
latexPostamblestringrequired
aiNotesstringrequired
sortFieldOrdinalintegerrequired>= 0
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
fieldsobject[]required
templatesobject[]required
PATCH

PATCH /note-types/:id/guide

Record how this note type's cards should be written. Changes no schema.

Prose guidance for agents and for humans — what a good card of this type looks like, what to put in each field. Read by the Scribe and by `get_deck_profile` over MCP. It touches no field, no template and no note, so it is safe to edit at any time.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
notesstringoptionalat most 2000 characters
fieldsobject[]optional

Request

curl -X PATCH "https://api.cardaga.in/note-types/{id}/guide" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
notesstringrequiredat most 2000 characters
fieldsobject[]required
POST

POST /note-types/:id/clone

Copy the structure under a new name. Notes are not copied.

The way to experiment with templates without risking existing notes: you get an empty note type with the same fields and templates, and the original is untouched.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
namestringrequired1–200 characters

Request

curl -X POST "https://api.cardaga.in/note-types/{id}/clone" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
iduuidrequired
namestringrequired
kindstringrequiredone of: standard | cloze
cssstringrequired
latexPreamblestringrequired
latexPostamblestringrequired
aiNotesstringrequired
sortFieldOrdinalintegerrequired>= 0
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
fieldsobject[]required
templatesobject[]required
POST

POST /note-types/:id/migration-preview

What a plan would change, orphan, add, and remove. Writes nothing.

Field restructuring is the one operation here that can lose data, so the preview is a first-class route rather than a flag. It reports which notes lose content, which cards would be orphaned, and which would be created — before anything happens.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
fieldOpsunknown[]required
templateOpsunknown[]required

Request

curl -X POST "https://api.cardaga.in/note-types/{id}/migration-preview" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
notesAffectedintegerrequired>= 0
fieldsOrphanedobject[]required
cardsAddedintegerrequired>= 0
cardsRemovedintegerrequired>= 0
POST

POST /note-types/:id/migrate

Apply a plan in one transaction, with an undo entry holding the inverse.

All of it or none of it, and reversible afterwards. Run `migrationPreview` with the same plan first: this route does not report what it is about to do, it does it.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
requestIdstringrequired8–64 characters
acknowledgeOrphansbooleanrequired
expectedPreviewobjectrequired
fieldOpsunknown[]required
templateOpsunknown[]required

Request

curl -X POST "https://api.cardaga.in/note-types/{id}/migrate" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
noteTypeobjectrequired
appliedobjectrequired
GET

GET /notes

The browser list, and search, which are the same query.

Keyset-paginated. The Browse screen and a `?q=` search hit this one route through one parser, so a query that works in the UI works here character for character — there is no second grammar to learn and none to drift.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200
qstringoptionalat most 4000 characters
deckIduuidoptional
noteTypeIduuidoptional
tagstringoptionalat most 500 characters
flaginteger | nulloptional>= 0, <= 7
statestringoptionalone of: new | learning | review | relearning
includeDeletedunknownoptional
sortstringoptionalone of: created | modified | sortField | noteType
directionstringoptionalone of: asc | desc

Request

curl "https://api.cardaga.in/notes" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
POST

POST /notes

Create a note, generating its cards.

Cards are generated from the note type’s templates, so a two-template type produces two cards and a cloze type produces one per distinct ordinal. The response reports what was generated rather than leaving the caller to infer it. A duplicate first field is allowed, and reported — see `duplicates`.

Request body

Request body
FieldTypeRequiredNotes
noteTypeIduuidrequired
deckIduuidrequired
fieldsobjectrequired
tagsstring[]optional
guidstringoptional1–100 characters
requestIdstringoptional8–64 characters

Request

curl -X POST "https://api.cardaga.in/notes" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
iduuidrequired
noteTypeIduuidrequired
guidstringrequired
fieldsobjectrequired
tagsstring[]required
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
deletedAtdate-time | nullrequired
cardIdsuuid[]required
GET

GET /notes/:id

Fetch one note with its fields and tags.

Field values are keyed by field id, not by name, because names are editable and ids are not. Resolve names through the note’s type. Another user’s note answers 404.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/notes/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
noteTypeIduuidrequired
guidstringrequired
fieldsobjectrequired
tagsstring[]required
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
deletedAtdate-time | nullrequired
cardIdsuuid[]required
PATCH

PATCH /notes/:id

Patch fields and/or tags. 409 when `ifUnmodifiedSince` is stale.

Editing a field can create or destroy cards: adding a cloze ordinal generates one, and emptying a field that a template is conditional on can orphan one. The response says what changed. `ifUnmodifiedSince` makes the write a compare-and-swap, which is what stops an agent’s stale edit from overwriting a human’s newer one.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
fieldsobjectoptional
tagsstring[]optional
ifUnmodifiedSincedate-timeoptional
requestIdstringoptional8–64 characters

Request

curl -X PATCH "https://api.cardaga.in/notes/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
noteTypeIduuidrequired
guidstringrequired
fieldsobjectrequired
tagsstring[]required
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
deletedAtdate-time | nullrequired
cardIdsuuid[]required
DELETE

DELETE /notes/:id

Delete a note and its cards.

A soft delete that is undoable and syncs as a tombstone, so a deletion made offline propagates rather than being resurrected by the next replica that has not heard about it.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
requestIdstringoptional8–64 characters

Request

curl -X DELETE "https://api.cardaga.in/notes/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
okbooleanrequiredone of: true
POST

POST /notes/bulk

Apply one action to many notes at once.

Every action reports `affected` before anything else, and every one writes a single undo entry so the whole batch reverses as one action rather than as N. Prefer this to looping: a thousand single calls is a thousand transactions and a thousand undo entries.

Request

curl -X POST "https://api.cardaga.in/notes/bulk" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
dryRunbooleanrequired
undoLabelstringoptional
POST

POST /notes/duplicates

Live duplicate detection while typing.

Matches on the first field, the same rule Anki uses, scoped to the note type. Built to be called on every keystroke: it reads an index and returns ids, and it is a POST only because the field value can be longer than a URL should carry.

Request body

Request body
FieldTypeRequiredNotes
noteTypeIduuidrequired
firstFieldstringrequiredat most 10000 characters

Request

curl -X POST "https://api.cardaga.in/notes/duplicates" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
noteIdsuuid[]required
GET

GET /cards

List cards, filtered and paginated.

Keyset-paginated. Cards are generated from notes by a note type — this route reads them, and nothing here changes what a note says. Filter by deck, state, flag or suspension; for anything richer use the browser query grammar at `GET /search/cards`.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200
qstringoptionalat most 4000 characters
deckIduuidoptional
noteIduuidoptional
statestringoptionalone of: new | learning | review | relearning
flaginteger | nulloptional>= 0, <= 7
suspendedunknownoptional
buriedunknownoptional
isLeechunknownoptional
dueBeforedate-timeoptional
sortstringoptionalone of: due | created | interval | lapses | reps | deck | position
directionstringoptionalone of: asc | desc

Request

curl "https://api.cardaga.in/cards" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
GET

GET /cards/:id

Fetch one card.

The card with its scheduling state — due date, interval, ease, lapses, queue position — and the note fields it renders from. Another user’s card answers 404, not 403.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/cards/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
cardobjectrequired
noteIduuidrequired
noteTypeIduuidrequired
questionHtmlstringrequired
answerHtmlstringrequired
answerIncludesQuestionbooleanrequired
cssstringrequired
tagsstring[]required
deckDisplayPathstringrequired
POST

POST /cards/suspend

Suspend cards, taking them out of the queue indefinitely.

A suspended card is never gathered, keeps its scheduling state untouched, and resumes exactly where it was when unsuspended. Batch: pass every id in one call rather than looping, and `affected` reports how many actually changed.

Request body

Request body
FieldTypeRequiredNotes
cardIdsuuid[]required
requestIdstringrequired8–64 characters

Request

curl -X POST "https://api.cardaga.in/cards/suspend" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
POST

POST /cards/unsuspend

Return suspended cards to the queue.

Restores the card to whatever state it held when suspended. A card whose due date passed while suspended becomes due immediately rather than being rescheduled — no time was served, and pretending otherwise would silently rewrite the user’s history.

Request body

Request body
FieldTypeRequiredNotes
cardIdsuuid[]required
requestIdstringrequired8–64 characters

Request

curl -X POST "https://api.cardaga.in/cards/unsuspend" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
POST

POST /cards/bury

Bury until the user’s next rollover, computed server-side.

Burying hides a card for the rest of the study day only. The rollover instant is computed from the caller’s own rollover hour and timezone, server-side, so a client in a different zone cannot bury a card until the wrong moment.

Request body

Request body
FieldTypeRequiredNotes
cardIdsuuid[]required
requestIdstringrequired8–64 characters

Request

curl -X POST "https://api.cardaga.in/cards/bury" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
POST

POST /cards/unbury

Unbury cards immediately, before rollover.

Clears both the buried-until instant and the reason, so a sibling buried by the answer of another card becomes available again in the same session.

Request body

Request body
FieldTypeRequiredNotes
cardIdsuuid[]required
requestIdstringrequired8–64 characters

Request

curl -X POST "https://api.cardaga.in/cards/unbury" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
POST

POST /cards/flag

Set or clear a colour flag on cards.

Flags are the user’s own marks and mean nothing to the scheduler — flagging changes nothing about when a card is next seen. Flag 0 clears. Batch, with `affected`.

Request body

Request body
FieldTypeRequiredNotes
cardIdsuuid[]required
requestIdstringrequired8–64 characters
flagintegerrequired>= 0, <= 7

Request

curl -X POST "https://api.cardaga.in/cards/flag" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
POST

POST /cards/reposition

Renumber new cards. `shiftExisting` renumbers everything after the start.

New cards are gathered in queue-position order, so this is how a caller controls what gets studied first. With `shiftExisting`, every new card at or after `start` is pushed along to make room, which touches far more rows than the ids you passed — the response reports the true count.

Request body

Request body
FieldTypeRequiredNotes
cardIdsuuid[]required
requestIdstringrequired8–64 characters
startPositionintegerrequired>= 0
stepintegerrequired>= 0, <= 1000
randomizebooleanrequired
shiftExistingbooleanrequired

Request

curl -X POST "https://api.cardaga.in/cards/reposition" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
POST

POST /cards/move

Move cards to another deck.

Scheduling is preserved: a card carries its own state and does not restart because it changed deck. What can change is which preset applies, and therefore its limits and burying behaviour, from the next queue build onward.

Request body

Request body
FieldTypeRequiredNotes
cardIdsuuid[]required
requestIdstringrequired8–64 characters
targetDeckIduuidrequired

Request

curl -X POST "https://api.cardaga.in/cards/move" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
GET

GET /card-skills

List saved card skills.

A card skill is a reusable AI instruction the user has written — “generate audio for the Back field”, “reformat until it fits on a phone” — that can be run over a whole deck.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200

Request

curl "https://api.cardaga.in/card-skills" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
POST

POST /card-skills

Create a card skill.

`instructions` is what to do per card and `doneWhen` is how the agent judges it finished; both are prose. `config` is validated against a schema per skill kind rather than accepted as a free object, so a misspelled voice fails here and not mid-run.

Request body

Request body
FieldTypeRequiredNotes
namestringrequired1–200 characters
instructionsstringrequired1–8000 characters
criteriastringrequired1–4000 characters
configobjectrequired

Request

curl -X POST "https://api.cardaga.in/card-skills" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
iduuidrequired
namestringrequired1–200 characters
instructionsstringrequired1–8000 characters
criteriastringrequired1–4000 characters
configobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
deletedAtdate-time | nullrequired
GET

GET /card-skills/:id

Fetch one card skill.

Its instructions, its definition of done, and its typed config: the voice and language for a speech skill, the target field for a formatting one.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/card-skills/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
namestringrequired1–200 characters
instructionsstringrequired1–8000 characters
criteriastringrequired1–4000 characters
configobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
deletedAtdate-time | nullrequired
PATCH

PATCH /card-skills/:id

Edit a card skill.

Affects future runs only. A run in flight carries the version of the skill it started with, so editing a skill cannot change what an executing batch is doing halfway through.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
namestringoptional1–200 characters
instructionsstringoptional1–8000 characters
criteriastringoptional1–4000 characters
configobjectoptional

Request

curl -X PATCH "https://api.cardaga.in/card-skills/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
namestringrequired1–200 characters
instructionsstringrequired1–8000 characters
criteriastringrequired1–4000 characters
configobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
deletedAtdate-time | nullrequired
DELETE

DELETE /card-skills/:id

Delete a card skill.

Past runs and the revisions they produced are kept — deleting the instruction does not delete the history of what it did, or the ability to undo it.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl -X DELETE "https://api.cardaga.in/card-skills/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
okbooleanrequiredone of: true
GET

GET /card-skills/voices

List the speech voices this deployment can use.

Read from the configured speech upstream, not hard-coded, so it reflects what is actually available. Returns an empty list when no speech service is configured, which is a supported deployment rather than an error.

Request

curl "https://api.cardaga.in/card-skills/voices" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
voicesobject[]required
GET

GET /skill-runs

List skill runs, newest first.

A run is one card skill executed across a deck. Keyset-paginated, with per-run counts of what succeeded, failed and was skipped.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200

Request

curl "https://api.cardaga.in/skill-runs" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
POST

POST /skill-runs

Start a card skill over a deck.

Queues the run and answers immediately with an id. The worker executes per card against the same MCP tool surface an external client would use, so a skill can do nothing a third-party agent could not.

Request body

Request body
FieldTypeRequiredNotes
skillIduuidrequired
deckIduuidoptional
noteIdsuuid[]optional

Request

curl -X POST "https://api.cardaga.in/skill-runs" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
iduuidrequired
skillIduuidrequired
deckIduuid | nullrequired
noteIdsuuid[]required
statusstringrequiredone of: queued | running | succeeded | failed | cancelled
progressPctintegerrequired>= 0, <= 100
totalsobjectrequired
reportobject[]required
errorstring | nullrequired
stalebooleanrequired
createdAtdate-timerequired
finishedAtdate-time | nullrequired
GET

GET /skill-runs/:id

Fetch one run with its per-card outcomes.

Every card the run touched, what happened to it, and the revision it produced. This is what makes a batch of AI edits auditable rather than something that just happened.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/skill-runs/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
skillIduuidrequired
deckIduuid | nullrequired
noteIdsuuid[]required
statusstringrequiredone of: queued | running | succeeded | failed | cancelled
progressPctintegerrequired>= 0, <= 100
totalsobjectrequired
reportobject[]required
errorstring | nullrequired
stalebooleanrequired
createdAtdate-timerequired
finishedAtdate-time | nullrequired
POST

POST /skill-runs/:id/cancel

Stop a running skill run.

Cards already processed keep their edits and stay undoable; cards not yet reached are skipped. Cancelling is not a rollback — use `undo` for that.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
requestIdstringrequired8–64 characters

Request

curl -X POST "https://api.cardaga.in/skill-runs/{id}/cancel" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
skillIduuidrequired
deckIduuid | nullrequired
noteIdsuuid[]required
statusstringrequiredone of: queued | running | succeeded | failed | cancelled
progressPctintegerrequired>= 0, <= 100
totalsobjectrequired
reportobject[]required
errorstring | nullrequired
stalebooleanrequired
createdAtdate-timerequired
finishedAtdate-time | nullrequired
POST

POST /skill-runs/:id/undo

Reverse every revision a run produced, as one action.

A run can touch hundreds of notes, so undoing it card by card is not a real option. This reverts the whole batch in one transaction, restoring each note’s prior snapshot.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
requestIdstringrequired8–64 characters

Request

curl -X POST "https://api.cardaga.in/skill-runs/{id}/undo" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
skillIduuidrequired
deckIduuid | nullrequired
noteIdsuuid[]required
statusstringrequiredone of: queued | running | succeeded | failed | cancelled
progressPctintegerrequired>= 0, <= 100
totalsobjectrequired
reportobject[]required
errorstring | nullrequired
stalebooleanrequired
createdAtdate-timerequired
finishedAtdate-time | nullrequired
GET

GET /queue/next

Due cards in queue order, with counts and the scheduling preset.

The whole study session in one read: the gathered cards in the order they should be shown, the new/learning/review counts behind them, and the resolved preset the client needs to render predicted intervals on the answer buttons. Honours the caller’s day rollover, suspension and burying.

Query parameters

Query parameters
FieldTypeRequiredNotes
deckIduuidrequired
limitintegeroptional>= 1, <= 50
lastAnsweredCardIduuidoptional
sessionAnchorstringoptional

Request

curl "https://api.cardaga.in/queue/next" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
cardsobject[]required
countsobjectrequired
presetobjectrequired
showTimerbooleanoptional
stopTimerOnAnswerbooleanoptional
learnAheadMinsintegeroptional>= 0
schedulerstringrequiredone of: fsrs | sm2
presetByDeckobjectoptional
balanceobjectoptional
serverNowdate-timerequired
nextRolloverAtdate-timerequired
nextReadyAtdate-time | nulloptional
sessionAnchorstringoptional
POST

POST /queue/answer

Commit one answer. Idempotent on `clientEventId`.

Writes the review, advances the card’s FSRS state, and buries siblings when the preset says to. `clientEventId` makes a retry free rather than merely harmless: replaying one returns the original result and schedules nothing twice, which is what lets an offline client flush a queue of answers without checking what landed.

Request body

Request body
FieldTypeRequiredNotes
cardIduuidrequired
ratingstringrequiredone of: again | hard | good | easy
durationMsintegerrequired>= 0, <= 3600000
clientEventIdstringrequired8–64 characters
answeredAtdate-timerequired

Request

curl -X POST "https://api.cardaga.in/queue/answer" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
replayedbooleanrequired
cardobjectrequired
reviewobjectrequired
countsobjectrequired
undoobjectrequired
POST

POST /queue/undo

Reverse the most recent answer.

Pops the newest entry off the undo stack and restores the card’s prior state wholesale rather than computing an inverse. 404 when the stack is empty. Pass `expectedReviewLogId` to make it a compare-and-swap: a mismatch is a 409 rather than undoing an answer the caller did not know about, which is what stops two devices undoing each other’s reviews.

Request body

Request body
FieldTypeRequiredNotes
expectedReviewLogIduuidoptional

Request

curl -X POST "https://api.cardaga.in/queue/undo" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
kindstringrequired
labelstringrequired
countsobjectrequired
GET

GET /reviews

Review history for a card or a deck.

The raw `review_log` rows — rating, interval before and after, duration, and when. Keyset paginated and append-only: this is the record the scheduler’s behaviour can be replayed from, so nothing here is ever rewritten.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200
cardIduuidoptional
fromdate-timeoptional
todate-timeoptional

Request

curl "https://api.cardaga.in/reviews" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
GET

GET /preferences

Where this account’s study day starts.

Timezone, rollover hour and the knobs beside them. An account that has never set anything reads as the defaults — UTC, a 04:00 rollover — rather than as missing, because the scheduler already falls back to exactly those.

Request

curl "https://api.cardaga.in/preferences" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
timezonestringrequiredat least 1 characters
rolloverHourintegerrequired>= 0, <= 23
learnAheadMinsintegerrequired>= 0
themestringrequired
schedulerstringrequiredone of: fsrs | sm2
loadBalancerEnabledbooleanrequired
newCardsIgnoreReviewLimitbooleanrequired
applyAllParentLimitsbooleanrequired
updatedAtdate-timerequired
PATCH

PATCH /preferences

Change the timezone or the rollover hour.

Send only what changed. A value equal to the stored one is a no-op that leaves the row untouched, so a client may report the browser’s zone on every sign-in — which the web app does — without churning sync. The zone must be one the server can resolve; a UTC offset is not a zone and is refused.

Request body

Request body
FieldTypeRequiredNotes
timezonestringoptional1–64 characters
rolloverHourintegeroptional>= 0, <= 23
learnAheadMinsintegeroptional>= 0, <= 1440

Request

curl -X PATCH "https://api.cardaga.in/preferences" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
timezonestringrequiredat least 1 characters
rolloverHourintegerrequired>= 0, <= 23
learnAheadMinsintegerrequired>= 0
themestringrequired
schedulerstringrequiredone of: fsrs | sm2
loadBalancerEnabledbooleanrequired
newCardsIgnoreReviewLimitbooleanrequired
applyAllParentLimitsbooleanrequired
updatedAtdate-timerequired
GET

GET /tags

List tags with their usage counts.

Hierarchical, `::`-separated, and independent of decks. `usageCount` is maintained by a trigger rather than counted per request, so this stays cheap on a large collection.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200
prefixstringoptionalat most 500 characters
parentIduuidoptional

Request

curl "https://api.cardaga.in/tags" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
PATCH

PATCH /tags/:id

Rewrite the tag and its subtree. Also re-parents, if the path moves.

One statement over the subtree, not a loop: renaming `spanish` to `Spanish` rewrites every descendant path with it. Because a tag’s identity IS its path, a rename whose new path sits under a different parent is also a move, which is why there is no separate re-parent route.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
displayPathstringrequired1–500 characters
requestIdstringoptional8–64 characters

Request

curl -X PATCH "https://api.cardaga.in/tags/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
parentIduuid | nullrequired
namestringrequired
pathstringrequired
displayPathstringrequired
usageCountintegerrequired>= 0
createdAtdate-timerequired
DELETE

DELETE /tags/:id

Delete a tag and its subtree.

Removes the tag from every note carrying it. The notes themselves are untouched: a tag is a label, and deleting a label has never been a reason to delete what it labelled.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
includeChildrenbooleanrequired
requestIdstringoptional8–64 characters

Request

curl -X DELETE "https://api.cardaga.in/tags/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
POST

POST /tags/merge

Move `note_tags` rows to the target, then drop the source subtree.

The fix for `spanish` versus `Spanish` after an import. Notes already carrying the target are not duplicated — `notesMoved` counts only the rows that actually gained it — and the source subtree is deleted once its notes are re-pointed.

Request body

Request body
FieldTypeRequiredNotes
sourceIduuidrequired
targetIduuidrequired
requestIdstringrequired8–64 characters

Request

curl -X POST "https://api.cardaga.in/tags/merge" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
affectedintegerrequired>= 0
GET

GET /search/suggest

Type-ahead for the token under the caret.

Given a partial query and a caret position, returns completions for the token being typed — deck names after `deck:`, tags after `tag:`, and the field names of the note type in scope. It parses the same grammar the search itself uses.

Query parameters

Query parameters
FieldTypeRequiredNotes
prefixstringrequiredat most 200 characters
limitintegeroptional>= 1, <= 50

Request

curl "https://api.cardaga.in/search/suggest" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
suggestionsobject[]required
GET

GET /stats/summary

The headline numbers for a collection or a deck.

Card counts by state, review totals, streak and retention — what the statistics screen shows above the charts. Day boundaries follow the caller’s rollover hour, not UTC.

Query parameters

Query parameters
FieldTypeRequiredNotes
deckIduuidoptional

Request

curl "https://api.cardaga.in/stats/summary" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
totalCardsintegerrequired>= 0
newCardsintegerrequired>= 0
learningCardsintegerrequired>= 0
reviewCardsintegerrequired>= 0
relearningCardsintegerrequired>= 0
suspendedCardsintegerrequired>= 0
buriedCardsintegerrequired>= 0
matureCardsintegerrequired>= 0
reviewsTodayintegerrequired>= 0
millisecondsTodayintegerrequired>= 0
studiedDaysintegerrequired>= 0
currentStreakDaysintegerrequired>= 0
GET

GET /stats/heatmap

Reviews per day, as the calendar grid renders them.

One bucket per study day over the requested range, counted against the caller’s own rollover so a session finished at 2am lands on the day the user considers it.

Query parameters

Query parameters
FieldTypeRequiredNotes
fromstringrequired
tostringrequired
deckIduuidoptional

Request

curl "https://api.cardaga.in/stats/heatmap" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
daysobject[]required
GET

GET /stats/forecast

Cards coming due per day, capped at a one-year horizon.

Projects existing due dates forward; it does not simulate answers, so it is a floor rather than a prediction — cards that lapse will reappear sooner than this says. The cap is deliberate: beyond a year the numbers are noise with a chart around them.

Query parameters

Query parameters
FieldTypeRequiredNotes
daysintegeroptional>= 1, <= 365
deckIduuidoptional
includeSuspendedbooleanoptional

Request

curl "https://api.cardaga.in/stats/forecast" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
daysobject[]required
GET

GET /stats/intervals

How current intervals are distributed across the collection.

Bucketed by interval length, which is what makes the difference between a healthy collection and one full of leeches visible at a glance. Every bucket is returned even when empty, so the shape of the chart does not change with the data.

Query parameters

Query parameters
FieldTypeRequiredNotes
deckIduuidoptional

Request

curl "https://api.cardaga.in/stats/intervals" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
bucketsobject[]required
averageDaysnumberrequired>= 0
medianDaysnumberrequired>= 0
GET

GET /stats/buttons

Answer counts split by review kind, because Again means two things.

Again on a learning card is an ordinary step; Again on a mature card is a lapse. Folded together they produce a retention number that flatters a collection nobody is retaining, so they are counted apart.

Query parameters

Query parameters
FieldTypeRequiredNotes
fromstringrequired
tostringrequired
deckIduuidoptional

Request

curl "https://api.cardaga.in/stats/buttons" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
rowsobject[]required
GET

GET /stats/true-retention

Pass rate over cards already in review, young and mature apart.

Excludes learning cards entirely. Including them is the most common way this number gets inflated — a card answered correctly thirty seconds after being shown has demonstrated nothing about retention.

Query parameters

Query parameters
FieldTypeRequiredNotes
deckIduuidoptional

Request

curl "https://api.cardaga.in/stats/true-retention" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
windowsobject[]required
GET

GET /stats/memory

What a break would cost: recall projected forward, and the reviews that pile up.

Every studied card’s forgetting curve, run forward day by day as if nothing were reviewed, summed into the number of cards you would still recall — plus the reviews that would be waiting on each day. Expected values from the scheduler’s own model, not a simulation. Reads `cards`, not the review log.

Query parameters

Query parameters
FieldTypeRequiredNotes
daysintegeroptional>= 1, <= 90
deckIduuidoptional

Request

curl "https://api.cardaga.in/stats/memory" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
cardsintegerrequired>= 0
recallnumber[]required
dueByinteger[]required
GET

GET /stats/decks

Every deck's recall against its own target, and how fast its new cards are met.

One row per deck, counting its own cards only: true retention over the last 30 study days, young and mature apart, with the deck’s effective target retention beside it; its studied and new card counts; and how many cards were answered for the first time over the last 28 study days. Reads the daily rollup and `cards`, never the review log.

Request

curl "https://api.cardaga.in/stats/decks" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
decksobject[]required
GET

GET /stats/time-sinks

Where review time went: the costliest cards, and how lopsided the time is.

Cards ranked by the answer time they took over the window, each answer counted for at most a minute; the cumulative share of time against the share of cards; and the five costliest cards with their whole answer history. The one stats route that reads the review log -- a per-card number the daily rollup cannot hold -- bounded to one user and the window.

Query parameters

Query parameters
FieldTypeRequiredNotes
daysintegeroptional>= 7, <= 365

Request

curl "https://api.cardaga.in/stats/time-sinks" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
totalMsintegerrequired>= 0
cardsReviewedintegerrequired>= 0
curveobject[]required
topobject[]required
GET

GET /cards/:cardId/insights

Thread roots on a card back, or a thread’s replies.

Insights are the tutor’s answers, persisted against the card rather than living in a chat window — which is the whole feature: they are still there on the next review. Without `threadId` you get the roots; with it, that thread’s replies in order.

Path parameters

Path parameters
FieldTypeRequiredNotes
cardIduuidrequired

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200
cardIduuidoptional
rootsOnlyunknownoptional
threadRootIduuidoptional
pinnedOnlyunknownoptional

Request

curl "https://api.cardaga.in/cards/{cardId}/insights" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
POST

POST /insights/threads

Ask the tutor about a card. Streams deltas, then the saved insight.

Server-sent events: token deltas as they arrive, then a final frame carrying the persisted insight with its id. The insight is written to Postgres before that frame, so a client that reloads mid-stream loses the animation and nothing else. Aborting the request cancels the upstream call rather than merely abandoning the response.

Request body

Request body
FieldTypeRequiredNotes
cardIduuidrequired
promptstringrequired1–4000 characters

Request

curl -X POST "https://api.cardaga.in/insights/threads" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body: none.

POST

POST /insights/:id/replies

Follow up in an existing thread. Streams the same frames.

Same SSE shape as `createThread`, with the thread’s prior turns as context. Cost is metered and recorded per request, failures included, so a thread that errors upstream still shows up in usage.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
promptstringrequired1–4000 characters

Request

curl -X POST "https://api.cardaga.in/insights/{id}/replies" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body: none.

PATCH

PATCH /insights/:id/pin

Pin or unpin an insight on the card back.

A pinned insight renders with the card during review instead of behind a disclosure. This is a display flag and nothing else: it changes no scheduling and no content.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
isPinnedbooleanrequired

Request

curl -X PATCH "https://api.cardaga.in/insights/{id}/pin" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
cardIduuidrequired
threadRootIduuid | nullrequired
parentIduuid | nullrequired
rolestringrequiredone of: user | assistant | action
contentMdstringrequired
summarystring | nullrequired
isPinnedbooleanrequired
modelstring | nullrequired
promptVersionstring | nullrequired
contextDigestobject | nullrequired
planobject | nullrequired
feedbackstring | nulloptionalone of: up | down
versionintegerrequired> 0
createdAtdate-timerequired
deletedAtdate-time | nullrequired
actionobject | nullrequired
PATCH

PATCH /insights/:id/feedback

Rate an answer thumbs-up or thumbs-down, or clear the rating.

Absolute, never a toggle: the body is what the rating should become, and `null` clears it. Only a tutor ANSWER can be rated -- the learner’s own question and an action chip are refused with 400. The rating is stored on the answer’s own row and syncs with it; a device with a local replica writes it there instead and this route is its fallback.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
feedbackstring | nullrequiredone of: up | down

Request

curl -X PATCH "https://api.cardaga.in/insights/{id}/feedback" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
cardIduuidrequired
threadRootIduuid | nullrequired
parentIduuid | nullrequired
rolestringrequiredone of: user | assistant | action
contentMdstringrequired
summarystring | nullrequired
isPinnedbooleanrequired
modelstring | nullrequired
promptVersionstring | nullrequired
contextDigestobject | nullrequired
planobject | nullrequired
feedbackstring | nulloptionalone of: up | down
versionintegerrequired> 0
createdAtdate-timerequired
deletedAtdate-time | nullrequired
actionobject | nullrequired
DELETE

DELETE /insights/:id

Delete an insight.

Deleting a thread root deletes its replies with it. There is no soft-delete here — an insight is generated content the user has explicitly rejected, and keeping a tombstone of it would be keeping the thing they asked to be rid of.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl -X DELETE "https://api.cardaga.in/insights/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
okbooleanrequiredone of: true
POST

POST /insights/:id/promote

Turn an insight into a real note, with its cards.

The path from “the tutor explained this well” to “I want to be tested on it”: creates a note of the chosen type in the chosen deck, generating cards by that type’s templates. The insight stays where it is; this copies out of it rather than moving.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
noteTypeIduuidrequired
deckIduuidrequired
fieldsobjectrequired
tagsstring[]optional
requestIdstringoptional8–64 characters

Request

curl -X POST "https://api.cardaga.in/insights/{id}/promote" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
iduuidrequired
noteTypeIduuidrequired
guidstringrequired
fieldsobjectrequired
tagsstring[]required
sourceSystemstringrequiredone of: cardagain | anki
sourceIdstring | nullrequired
metadataobjectrequired
createdAtdate-timerequired
updatedAtdate-timerequired
deletedAtdate-time | nullrequired
cardIdsuuid[]required
GET

GET /revisions

List revisions, newest first.

Every agent edit, versioned and reversible. Filter by status to get the pending queue a user has to act on, or by note to get one note’s history. Keyset-paginated.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200
noteIduuidoptional
deckIduuidoptional
noteInDeckIduuidoptional
statusstringoptionalone of: proposed | applied | rejected | reverted
actorstringoptionalone of: user | scribe | import
runIduuidoptional

Request

curl "https://api.cardaga.in/revisions" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
GET

GET /revisions/:id

Fetch one revision with its diff and prior snapshot.

The per-field diff is what the accept/reject UI renders; `beforeSnapshot` is the full prior state that `revert` restores. Both are stored rather than recomputed, so what a user is shown is exactly what will be applied.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/revisions/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
noteIduuid | nullrequired
deckIduuid | nullrequired
noteTypeIduuid | nullrequired
statusstringrequiredone of: proposed | applied | rejected | reverted
actorstringrequiredone of: user | scribe | import
toolNamestring | nullrequired
requestIdstringrequired
diffobject[]required
beforeSnapshotobjectrequired
appliedAtdate-time | nullrequired
revertedAtdate-time | nullrequired
createdAtdate-timerequired
runIduuid | nullrequired
POST

POST /revisions/:id/accept

Apply a proposed revision.

Applies the plan stored on the row — the same plan the user was shown — rather than recomputing one, because a second computation could drift from the diff they approved. A 409 when the note changed underneath it, never a last-write-wins: silently overwriting a human’s edit with an agent’s stale one is how people stop trusting this.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
requestIdstringrequired8–64 characters

Request

curl -X POST "https://api.cardaga.in/revisions/{id}/accept" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
noteIduuid | nullrequired
deckIduuid | nullrequired
noteTypeIduuid | nullrequired
statusstringrequiredone of: proposed | applied | rejected | reverted
actorstringrequiredone of: user | scribe | import
toolNamestring | nullrequired
requestIdstringrequired
diffobject[]required
beforeSnapshotobjectrequired
appliedAtdate-time | nullrequired
revertedAtdate-time | nullrequired
createdAtdate-timerequired
runIduuid | nullrequired
POST

POST /revisions/:id/reject

Reject a proposed revision without applying it.

The row stays as a record that the proposal was made and refused. `reason` is accepted and deliberately not stored: the revision table is history of what happened, not commentary on what did not.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
requestIdstringrequired8–64 characters
reasonstringoptionalat most 2000 characters

Request

curl -X POST "https://api.cardaga.in/revisions/{id}/reject" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
noteIduuid | nullrequired
deckIduuid | nullrequired
noteTypeIduuid | nullrequired
statusstringrequiredone of: proposed | applied | rejected | reverted
actorstringrequiredone of: user | scribe | import
toolNamestring | nullrequired
requestIdstringrequired
diffobject[]required
beforeSnapshotobjectrequired
appliedAtdate-time | nullrequired
revertedAtdate-time | nullrequired
createdAtdate-timerequired
runIduuid | nullrequired
POST

POST /revisions/decide

Accept or reject up to 100 proposals in one request.

Exactly what pressing each proposal’s own button would do, done server-side without the round trips: the same apply path, the same drift check, the same per-proposal revert afterwards. Refusals are reported per id and never stop the rest.

Request body

Request body
FieldTypeRequiredNotes
requestIdstringrequired8–64 characters
verdictstringrequiredone of: accept | reject
idsuuid[]required

Request

curl -X POST "https://api.cardaga.in/revisions/decide" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
decideduuid[]required
decidedCardsintegerrequired>= 0
refusedobject[]required
POST

POST /revisions/:id/revert

Undo an applied revision, restoring the prior snapshot.

Restores `beforeSnapshot` wholesale rather than computing an inverse diff, so revert is one action that cannot drift from what was applied. 409 if the target changed since.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
requestIdstringrequired8–64 characters

Request

curl -X POST "https://api.cardaga.in/revisions/{id}/revert" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
noteIduuid | nullrequired
deckIduuid | nullrequired
noteTypeIduuid | nullrequired
statusstringrequiredone of: proposed | applied | rejected | reverted
actorstringrequiredone of: user | scribe | import
toolNamestring | nullrequired
requestIdstringrequired
diffobject[]required
beforeSnapshotobjectrequired
appliedAtdate-time | nullrequired
revertedAtdate-time | nullrequired
createdAtdate-timerequired
runIduuid | nullrequired
POST

POST /revisions/:id/reapply

Redo a reverted revision.

The Redo half of the chat’s Undo/Redo pair: takes a reverted revision back to applied. 409 on any drift since the revert, for the same reason accept has one.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
requestIdstringrequired8–64 characters

Request

curl -X POST "https://api.cardaga.in/revisions/{id}/reapply" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
noteIduuid | nullrequired
deckIduuid | nullrequired
noteTypeIduuid | nullrequired
statusstringrequiredone of: proposed | applied | rejected | reverted
actorstringrequiredone of: user | scribe | import
toolNamestring | nullrequired
requestIdstringrequired
diffobject[]required
beforeSnapshotobjectrequired
appliedAtdate-time | nullrequired
revertedAtdate-time | nullrequired
createdAtdate-timerequired
runIduuid | nullrequired
DELETE

DELETE /account

Delete this account and everything in it, permanently.

Removes the collection — decks, note types, notes, cards, tags, media, insights, revisions — the review history, the sessions, the API tokens and the account row itself. Nothing is recoverable afterwards and there is no undo: the undo stack goes with the rest of it. The body must carry `confirmation` set to this account’s own e-mail address, compared against the session’s user server-side; anything else is a 400 and deletes nothing. The response is a census of what was removed, taken inside the deleting transaction, and the session cookie is dead the moment it returns.

Request body

Request body
FieldTypeRequiredNotes
confirmationstringrequired1–320 characters

Request

curl -X DELETE "https://api.cardaga.in/account" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
emailstringrequired
removedobjectrequired
GET

GET /ingest/sessions

List ingest sessions, newest first.

Keyset-paginated. Includes finished and cancelled sessions, so a user can find what a run produced days later.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200
statusstringoptionalone of: running | awaiting_user | done | failed | cancelled

Request

curl "https://api.cardaga.in/ingest/sessions" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
POST

POST /ingest/sessions

Turn a document into cards. Streams the turn and persists it.

Server-sent events while the agent reads the source and proposes notes. The session is persisted as it goes, so a dropped connection loses the stream and not the work — reopen it with `get`.

Request body

Request body
FieldTypeRequiredNotes
sourceunknownrequired
instructionsstringrequiredat most 2000 characters
deckIduuidoptional
guidanceobjectoptional
advancedStylingbooleanoptional

Request

curl -X POST "https://api.cardaga.in/ingest/sessions" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body: none.

POST

POST /ingest/sessions/:id/answer

Answer a pending question and run the next turn.

The agent stops and asks when the source is ambiguous — which deck, which note type, whether a passage is one card or five. This answers that question and streams the turn it unblocks, in the same SSE shape as `run`.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl -X POST "https://api.cardaga.in/ingest/sessions/{id}/answer" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body: none.

POST

POST /ingest/sessions/:id/tool-results

Answer a tool-call frame for a client-held document.

For a run started from a `client` source the agent's document tools run in the browser. Each `tool-call` frame on the run's stream names a tool and a `callId`; this delivers what the browser computed, and the turn continues on the same stream. A `callId` nothing is waiting on is 404.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
callIdstringrequiredat least 1 characters
resultobjectrequired

Request

curl -X POST "https://api.cardaga.in/ingest/sessions/{id}/tool-results" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
acceptedbooleanrequiredone of: true
GET

GET /ingest/sessions/:id/document

The session's extracted document, for the tab that executes its code tools.

Owner-scoped. For an uploaded/text/url run the server holds the extracted document; model-written code executes in the browser (plan v2), so the tab fetches this once and answers the run's tool-call frames over it. 404 for a session with no document (knowledge, client-held) exactly as for a session that does not exist.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/ingest/sessions/{id}/document" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
documentobjectrequired
GET

GET /ingest/sessions/:id

Reopen a run: its status, its pending question, its deck.

Everything needed to resume a session in a new tab: what stage it is at, what it is waiting on, and what it has produced so far.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/ingest/sessions/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
statusstringrequiredone of: running | awaiting_user | done | failed | cancelled
sourceunknownrequired
instructionsstringrequired
deckIduuid | nullrequired
guidanceobjectoptional
pendingQuestionunknown | nullrequired
stepsobjectrequired
errorstring | nullrequired
feedbackstring | nulloptionalone of: up | down
createdAtdate-timerequired
updatedAtdate-timerequired
POST

POST /ingest/sessions/:id/cancel

Cancel a running ingest session.

Aborts the upstream call rather than merely abandoning the response, so a cancelled session stops costing money. Notes already created are kept — cancelling stops the work, it does not undo it.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl -X POST "https://api.cardaga.in/ingest/sessions/{id}/cancel" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
statusstringrequiredone of: running | awaiting_user | done | failed | cancelled
sourceunknownrequired
instructionsstringrequired
deckIduuid | nullrequired
guidanceobjectoptional
pendingQuestionunknown | nullrequired
stepsobjectrequired
errorstring | nullrequired
feedbackstring | nulloptionalone of: up | down
createdAtdate-timerequired
updatedAtdate-timerequired
PATCH

PATCH /ingest/sessions/:id/feedback

Rate a finished run thumbs-up or thumbs-down, or clear the rating.

Absolute, never a toggle: the body is what the rating should become, and `null` clears it. Owner-scoped. 409 while the run is still running or waiting on the learner -- only a run that has ended (`done` or `failed`) has an answer to rate.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request body

Request body
FieldTypeRequiredNotes
feedbackstring | nullrequiredone of: up | down

Request

curl -X PATCH "https://api.cardaga.in/ingest/sessions/{id}/feedback" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
statusstringrequiredone of: running | awaiting_user | done | failed | cancelled
sourceunknownrequired
instructionsstringrequired
deckIduuid | nullrequired
guidanceobjectoptional
pendingQuestionunknown | nullrequired
stepsobjectrequired
errorstring | nullrequired
feedbackstring | nulloptionalone of: up | down
createdAtdate-timerequired
updatedAtdate-timerequired
GET

GET /imports

List import jobs, newest first.

Keyset-paginated. Jobs are kept after completion so their reports stay readable — an import is the operation a user most wants to audit weeks later.

Query parameters

Query parameters
FieldTypeRequiredNotes
cursorstringoptionalat least 1 characters
limitintegeroptional>= 1, <= 200
statusstringoptionalone of: queued | running | succeeded | failed | cancelled

Request

curl "https://api.cardaga.in/imports" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
itemsobject[]required
nextCursorstring | nullrequiredat least 1 characters
POST

POST /imports

Queue an .apkg/.colpkg import for an already-uploaded archive.

The SECOND of two requests. Stream the bytes to `POST /imports/archive` first, which is a plain binary route and answers with a content digest; name that digest here. Splitting them is what makes a 500MB `.colpkg` tractable — a single multipart request would need the whole archive buffered before its metadata could be validated — and content addressing makes a retried upload free rather than a second copy.

Request

curl -X POST "https://api.cardaga.in/imports" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
iduuidrequired
filenamestringrequired
sizeBytesintegerrequired>= 0
statusstringrequiredone of: queued | running | succeeded | failed | cancelled
progressPctintegerrequired>= 0, <= 100
stagestring | nullrequired
reportobject | nullrequired
errorstring | nullrequired
createdAtdate-timerequired
finishedAtdate-time | nullrequired
GET

GET /imports/limits

What the server accepts, so the browser can refuse a file instantly.

Maximum archive size and the accepted extensions. Fetch it before the picker: telling someone their 900MB file is too large after they have uploaded it for ten minutes is a worse product than telling them before.

Request

curl "https://api.cardaga.in/imports/limits" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
maxArchiveBytesinteger | nullrequired> 0
GET

GET /imports/:id

Stage and percentage while running; the terminal status after.

Poll this while an import runs. Progress is real work completed rather than an animation, so a large archive genuinely sits at a stage for a while. The terminal status is `succeeded` or `failed`, and either way the report is then readable.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/imports/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
filenamestringrequired
sizeBytesintegerrequired>= 0
statusstringrequiredone of: queued | running | succeeded | failed | cancelled
progressPctintegerrequired>= 0, <= 100
stagestring | nullrequired
reportobject | nullrequired
errorstring | nullrequired
createdAtdate-timerequired
finishedAtdate-time | nullrequired
GET

GET /imports/:id/report

Counts, mappings, missing media, fallbacks, and every skipped row.

The audit trail: what came in, what was mapped to what, which media files the archive referenced but did not contain, and every row that was skipped with the reason. This is the difference between “your import finished” and “here is exactly what happened to six years of review history”.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/imports/{id}/report" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
countsobjectrequired
noteTypesMappedobject[]required
mediaResolvedintegerrequired>= 0
mediaMissingstring[]required
templatesWithFallbackobject[]required
skippedobject[]required
timezoneCheckobjectoptional
schedulerstringoptionalone of: fsrs | sm2
presetsAdjustedobject[]optional
POST

POST /exports

Queue an export of a collection or a deck.

Runs as a background job because building an `.apkg` means reading every note, every card and every referenced media file. Answers immediately with a job id to poll.

Request body

Request body
FieldTypeRequiredNotes
deckIdsuuid[]optional
includeMediabooleanrequired
includeSchedulingbooleanrequired
includeDeckPresetsbooleanoptional
includeAiNotesbooleanoptional
legacySupportbooleanoptional
formatstringoptionalone of: apkg | colpkg

Request

curl -X POST "https://api.cardaga.in/exports" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

201

201 response body
FieldTypeRequiredNotes
iduuidrequired
statusstringrequiredone of: queued | running | succeeded | failed | cancelled
progressPctintegerrequired>= 0, <= 100
filenamestring | nullrequired
sizeBytesinteger | nullrequired>= 0
errorstring | nullrequired
createdAtdate-timerequired
finishedAtdate-time | nullrequired
GET

GET /exports/:id

Progress while the export runs; the terminal status after.

Poll until `succeeded`, then download. A failed export reports why rather than leaving a job that never finishes and a user who cannot tell the difference.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/exports/{id}" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
iduuidrequired
statusstringrequiredone of: queued | running | succeeded | failed | cancelled
progressPctintegerrequired>= 0, <= 100
filenamestring | nullrequired
sizeBytesinteger | nullrequired>= 0
errorstring | nullrequired
createdAtdate-timerequired
finishedAtdate-time | nullrequired
GET

GET /exports/:id/download

Download the finished archive.

Streams the `.apkg`, which Anki can open directly — the point of the format is that leaving is possible. 404 until the job has succeeded, so a caller cannot download a half-built archive and discover the problem later.

Path parameters

Path parameters
FieldTypeRequiredNotes
iduuidrequired

Request

curl "https://api.cardaga.in/exports/{id}/download" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body: none.

GET

GET /sync/meta

Who am I talking to, and can we sync at all?

The first call of every session, and a refusal here is cheaper than a divergence later: the hub compares protocol version, schema version and collection identity, and rejects outright rather than exchanging rows two replicas will disagree about.

Request

curl "https://api.cardaga.in/sync/meta" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
protocolVersionintegerrequired> 0
migrationHashstringrequiredat least 1 characters
scmdate-timerequired
maxUsnintegerrequired>= 0
mediaMaxUsnintegerrequired>= 0
fullSyncRequiredbooleanrequired
serverNowdate-timeoptional
POST

POST /sync/graves

Exchange deletion tombstones. Applied before changes, on both sides.

Before changes, deliberately and on both sides: a delete has to beat a concurrent edit, and applying edits first would resurrect a row the other replica already removed.

Request body

Request body
FieldTypeRequiredNotes
gravesobject[]required
afterintegerrequired>= 0

Request

curl -X POST "https://api.cardaga.in/sync/graves" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
gravesobject[]required
assignedobject[]required
GET

GET /sync/changes

Keyset page of hub rows with usn > after, one entity at a time.

Keyset rather than offset, and one entity per page, so a pull needs no server-side session and can resume from its cursor after a dropped connection — which on a phone is the normal case rather than the exception.

Query parameters

Query parameters
FieldTypeRequiredNotes
entitystringrequiredone of: note_type | note_type_field | card_template | deck_option_preset | deck | note | tag | note_tag | card | review_log | revision | insight | card_skill | user_preferences | media_asset | note_media
afterinteger | nullrequired>= 0
limitintegeroptional>= 1, <= 1000

Request

curl "https://api.cardaga.in/sync/changes" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
rowsobject[]required
nextAfterintegerrequired>= 0
POST

POST /sync/changes

Push pending rows; the hub assigns their USNs. Idempotent per batch.

The hub assigns update-sequence numbers, so a spoke never invents one and two spokes cannot collide. Replaying a batch is safe, which is what lets a client that lost its connection mid-push simply send it again.

Request body

Request body
FieldTypeRequiredNotes
entitystringrequiredone of: note_type | note_type_field | card_template | deck_option_preset | deck | note | tag | note_tag | card | review_log | revision | insight | card_skill | user_preferences | media_asset | note_media
rowsobject[]required

Request

curl -X POST "https://api.cardaga.in/sync/changes" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
assignedobject[]required
conflictedobject[]optional
POST

POST /sync/finish

Compare sanity summaries; a mismatch demands a full sync.

A checksum over the synced tables at the end of a session. A mismatch sets `fullSyncRequired` rather than being logged and forgotten, because divergence between two replicas compounds silently and the cheapest moment to catch it is now.

Request body

Request body
FieldTypeRequiredNotes
summaryobjectrequired

Request

curl -X POST "https://api.cardaga.in/sync/finish" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
okbooleanrequired
maxUsnintegerrequired>= 0
fullSyncRequiredbooleanrequired
POST

POST /sync/full

One-way full sync through the transfer pipeline (§4.3).

The recovery path when incremental sync cannot converge. It is explicitly one-way and the caller states the direction: there is no merge here, one side’s collection replaces the other’s, and pretending otherwise would lose data quietly instead of loudly.

Request body

Request body
FieldTypeRequiredNotes
directionstringrequiredone of: upload | download
uploadIduuidoptional

Request

curl -X POST "https://api.cardaga.in/sync/full" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

202

202 response body
FieldTypeRequiredNotes
jobIduuidrequired
POST

POST /sync/media/wanted

Which of these blobs the hub lacks bytes for — the upload filter.

Bounded (max 256 digests per call). Answers only for digests the caller owns a media_asset row for; anything else is reported as not wanted, indistinguishable from "already stored", so blob existence never leaks across users. The bytes then move over the ordinary media routes: rows travel by row sync FIRST, bytes follow — an upload before its row is pushed would mint a hub-side row whose id the pushed row then collides with.

Request body

Request body
FieldTypeRequiredNotes
sha256sstring[]required

Request

curl -X POST "https://api.cardaga.in/sync/media/wanted" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
wantedstring[]required
GET

GET /sync/whoami

The identity behind the presented credential — the collection identity.

Which user and which collection this token resolves to. A client uses it to notice it has been pointed at a different account before it starts writing rows into the wrong one.

Request

curl "https://api.cardaga.in/sync/whoami" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
userIduuidrequired
emailstringrequiredat least 1 characters
GET

GET /sync/status

Spoke-local sync facts: role, pending counts, last sync, flags.

Answered from the spoke’s own state, not the hub’s — how many local rows are waiting, when the last session finished, and whether `fullSyncRequired` is set. This is what the sync indicator in the UI reads.

Request

curl "https://api.cardaga.in/sync/status" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
rolestringrequiredone of: hub | spoke
peerConfiguredbooleanrequired
pendingRowsintegerrequired>= 0
pendingGravesintegerrequired>= 0
lastSyncAtdate-time | nullrequired
fullSyncRequiredbooleanrequired
POST

POST /sync/run

Drive one SyncSession against the configured peer. 409 when there is none.

Runs a whole session — meta, graves, pull, push, finish — and reports what it did. The 409 is for a spoke with no peer configured, which is a configuration problem and not a transient one worth retrying.

Request

curl -X POST "https://api.cardaga.in/sync/run" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body: none.

GET

GET /.well-known/apple-app-site-association

iOS's Universal Links file for app-link association.

Names the Apple Developer Team ID and bundle id this deployment vouches for, and the reset-password / verify-email paths iOS should hand to the app instead of Safari.

Request

curl "https://api.cardaga.in/.well-known/apple-app-site-association" \
  -H "Authorization: Bearer $CARDAGAIN_API_KEY"

Responses

200

200 response body
FieldTypeRequiredNotes
applinksobjectrequired