Cardagain API
v1.0.0 · OpenAPI 3.0.3 · openapi.json
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
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | required | 1–120 characters |
| idToken | object | optional | |
| handoff | object | optional |
Request
curl -X POST "https://api.cardaga.in/auth/device-tokens" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| token | string | required | at least 1 characters |
| expiresAt | date-time | required | |
| userId | uuid | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| revoked | boolean | required | one of: true |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| codeChallenge | string | required |
Request
curl -X POST "https://api.cardaga.in/auth/device-handoffs" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| code | string | required | at least 1 characters |
| expiresAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| payload | string | required | at least 1 characters |
Request
curl -X POST "https://api.cardaga.in/auth/apple/notifications" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| received | boolean | required | one of: true |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
| parentId | uuid | optional | |
| q | string | optional | at most 500 characters |
| includeFiltered | unknown | optional |
Request
curl "https://api.cardaga.in/decks" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | required | 1–200 characters |
| parentId | uuid | null | optional | |
| description | string | optional | at most 10000 characters |
| optionsPresetId | uuid | null | optional | |
| isFiltered | boolean | optional | |
| filterConfig | object | optional |
Request
curl -X POST "https://api.cardaga.in/decks" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| parentId | uuid | null | required | |
| name | string | required | |
| path | string | required | |
| displayPath | string | required | |
| optionsPresetId | uuid | null | required | |
| description | string | required | |
| isCollapsed | boolean | required | |
| isFiltered | boolean | required | |
| filterConfig | object | null | required | |
| newLimit | integer | null | optional | >= 0 |
| reviewLimit | integer | null | optional | >= 0 |
| newLimitToday | object | null | optional | |
| reviewLimitToday | object | null | optional | |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| nodes | unknown[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/decks/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| parentId | uuid | null | required | |
| name | string | required | |
| path | string | required | |
| displayPath | string | required | |
| optionsPresetId | uuid | null | required | |
| description | string | required | |
| isCollapsed | boolean | required | |
| isFiltered | boolean | required | |
| filterConfig | object | null | required | |
| newLimit | integer | null | optional | >= 0 |
| reviewLimit | integer | null | optional | >= 0 |
| newLimitToday | object | null | optional | |
| reviewLimitToday | object | null | optional | |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | optional | 1–200 characters |
| description | string | optional | at most 10000 characters |
| optionsPresetId | uuid | null | optional | |
| isCollapsed | boolean | optional | |
| filterConfig | object | null | optional | |
| newLimit | integer | null | optional | >= 0 |
| reviewLimit | integer | null | optional | >= 0 |
| newLimitToday | object | null | optional | |
| reviewLimitToday | object | null | optional |
Request
curl -X PATCH "https://api.cardaga.in/decks/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| parentId | uuid | null | required | |
| name | string | required | |
| path | string | required | |
| displayPath | string | required | |
| optionsPresetId | uuid | null | required | |
| description | string | required | |
| isCollapsed | boolean | required | |
| isFiltered | boolean | required | |
| filterConfig | object | null | required | |
| newLimit | integer | null | optional | >= 0 |
| reviewLimit | integer | null | optional | >= 0 |
| newLimitToday | object | null | optional | |
| reviewLimitToday | object | null | optional | |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl -X DELETE "https://api.cardaga.in/decks/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| affected | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| newParentId | uuid | null | required | |
| requestId | string | optional | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/decks/{id}/reposition" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| parentId | uuid | null | required | |
| name | string | required | |
| path | string | required | |
| displayPath | string | required | |
| optionsPresetId | uuid | null | required | |
| description | string | required | |
| isCollapsed | boolean | required | |
| isFiltered | boolean | required | |
| filterConfig | object | null | required | |
| newLimit | integer | null | optional | >= 0 |
| reviewLimit | integer | null | optional | >= 0 |
| newLimitToday | object | null | optional | |
| reviewLimitToday | object | null | optional | |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
Request
curl "https://api.cardaga.in/deck-presets" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | required | 1–200 characters |
| newPerDay | integer | optional | >= 0, <= 9999 |
| reviewsPerDay | integer | optional | >= 0, <= 99999 |
| learningStepsMins | number[] | optional | |
| relearningStepsMins | number[] | optional | |
| graduatingIntervalDays | integer | optional | >= 1 |
| easyIntervalDays | integer | optional | >= 1 |
| startingEase | number | optional | >= 1.3, <= 5 |
| maxIntervalDays | integer | optional | >= 1, <= 36500 |
| leechThreshold | integer | optional | >= 1, <= 99 |
| capAnswerTimeSecs | integer | optional | >= 1, <= 9999 |
| leechAction | string | optional | one of: suspend | tag_only |
| buryNewSiblings | boolean | optional | |
| buryReviewSiblings | boolean | optional | |
| newCardGatherOrder | string | optional | one of: due | random | position |
| reviewSortOrder | string | optional | one of: due_date_random | due_date_deck | ascending_intervals | descending_intervals | relative_overdueness |
| newMix | string | optional | one of: mix_with_reviews | after_reviews | before_reviews |
| interdayLearningMix | string | optional | one of: mix_with_reviews | after_reviews | before_reviews |
| showTimer | boolean | optional | |
| stopTimerOnAnswer | boolean | optional | |
| scheduler | string | optional | one of: fsrs | sm2 |
| desiredRetention | number | optional | >= 0.7, <= 0.99 |
| hardMultiplier | number | optional | >= 0.5, <= 1.3 |
| easyMultiplier | number | optional | >= 1, <= 5 |
| intervalMultiplier | number | optional | >= 0.5, <= 2 |
| lapseMultiplier | number | optional | >= 0, <= 1 |
| minimumLapseIntervalDays | integer | optional | >= 1, <= 36500 |
| fsrsWeights | number[] | optional | |
| easyDaysPercentages | number[] | optional |
Request
curl -X POST "https://api.cardaga.in/deck-presets" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| name | string | required | |
| newPerDay | integer | required | |
| reviewsPerDay | integer | required | |
| learningStepsMins | number[] | required | |
| relearningStepsMins | number[] | required | |
| graduatingIntervalDays | integer | required | |
| easyIntervalDays | integer | required | |
| startingEase | number | required | |
| maxIntervalDays | integer | required | |
| leechThreshold | integer | required | |
| capAnswerTimeSecs | integer | required | |
| leechAction | string | required | one of: suspend | tag_only |
| buryNewSiblings | boolean | required | |
| buryReviewSiblings | boolean | required | |
| newCardGatherOrder | string | required | one of: due | random | position |
| reviewSortOrder | string | required | one of: due_date_random | due_date_deck | ascending_intervals | descending_intervals | relative_overdueness |
| newMix | string | required | one of: mix_with_reviews | after_reviews | before_reviews |
| interdayLearningMix | string | required | one of: mix_with_reviews | after_reviews | before_reviews |
| showTimer | boolean | required | |
| stopTimerOnAnswer | boolean | required | |
| scheduler | string | required | one of: fsrs | sm2 |
| desiredRetention | number | required | |
| hardMultiplier | number | required | |
| easyMultiplier | number | required | |
| intervalMultiplier | number | required | |
| lapseMultiplier | number | required | |
| minimumLapseIntervalDays | integer | required | |
| fsrsWeights | number[] | required | |
| easyDaysPercentages | number[] | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/deck-presets/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| name | string | required | |
| newPerDay | integer | required | |
| reviewsPerDay | integer | required | |
| learningStepsMins | number[] | required | |
| relearningStepsMins | number[] | required | |
| graduatingIntervalDays | integer | required | |
| easyIntervalDays | integer | required | |
| startingEase | number | required | |
| maxIntervalDays | integer | required | |
| leechThreshold | integer | required | |
| capAnswerTimeSecs | integer | required | |
| leechAction | string | required | one of: suspend | tag_only |
| buryNewSiblings | boolean | required | |
| buryReviewSiblings | boolean | required | |
| newCardGatherOrder | string | required | one of: due | random | position |
| reviewSortOrder | string | required | one of: due_date_random | due_date_deck | ascending_intervals | descending_intervals | relative_overdueness |
| newMix | string | required | one of: mix_with_reviews | after_reviews | before_reviews |
| interdayLearningMix | string | required | one of: mix_with_reviews | after_reviews | before_reviews |
| showTimer | boolean | required | |
| stopTimerOnAnswer | boolean | required | |
| scheduler | string | required | one of: fsrs | sm2 |
| desiredRetention | number | required | |
| hardMultiplier | number | required | |
| easyMultiplier | number | required | |
| intervalMultiplier | number | required | |
| lapseMultiplier | number | required | |
| minimumLapseIntervalDays | integer | required | |
| fsrsWeights | number[] | required | |
| easyDaysPercentages | number[] | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | optional | 1–200 characters |
| newPerDay | integer | optional | >= 0, <= 9999 |
| reviewsPerDay | integer | optional | >= 0, <= 99999 |
| learningStepsMins | number[] | optional | |
| relearningStepsMins | number[] | optional | |
| graduatingIntervalDays | integer | optional | >= 1 |
| easyIntervalDays | integer | optional | >= 1 |
| startingEase | number | optional | >= 1.3, <= 5 |
| maxIntervalDays | integer | optional | >= 1, <= 36500 |
| leechThreshold | integer | optional | >= 1, <= 99 |
| capAnswerTimeSecs | integer | optional | >= 1, <= 9999 |
| leechAction | string | optional | one of: suspend | tag_only |
| buryNewSiblings | boolean | optional | |
| buryReviewSiblings | boolean | optional | |
| newCardGatherOrder | string | optional | one of: due | random | position |
| reviewSortOrder | string | optional | one of: due_date_random | due_date_deck | ascending_intervals | descending_intervals | relative_overdueness |
| newMix | string | optional | one of: mix_with_reviews | after_reviews | before_reviews |
| interdayLearningMix | string | optional | one of: mix_with_reviews | after_reviews | before_reviews |
| showTimer | boolean | optional | |
| stopTimerOnAnswer | boolean | optional | |
| scheduler | string | optional | one of: fsrs | sm2 |
| desiredRetention | number | optional | >= 0.7, <= 0.99 |
| hardMultiplier | number | optional | >= 0.5, <= 1.3 |
| easyMultiplier | number | optional | >= 1, <= 5 |
| intervalMultiplier | number | optional | >= 0.5, <= 2 |
| lapseMultiplier | number | optional | >= 0, <= 1 |
| minimumLapseIntervalDays | integer | optional | >= 1, <= 36500 |
| fsrsWeights | number[] | optional | |
| easyDaysPercentages | number[] | optional |
Request
curl -X PATCH "https://api.cardaga.in/deck-presets/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| name | string | required | |
| newPerDay | integer | required | |
| reviewsPerDay | integer | required | |
| learningStepsMins | number[] | required | |
| relearningStepsMins | number[] | required | |
| graduatingIntervalDays | integer | required | |
| easyIntervalDays | integer | required | |
| startingEase | number | required | |
| maxIntervalDays | integer | required | |
| leechThreshold | integer | required | |
| capAnswerTimeSecs | integer | required | |
| leechAction | string | required | one of: suspend | tag_only |
| buryNewSiblings | boolean | required | |
| buryReviewSiblings | boolean | required | |
| newCardGatherOrder | string | required | one of: due | random | position |
| reviewSortOrder | string | required | one of: due_date_random | due_date_deck | ascending_intervals | descending_intervals | relative_overdueness |
| newMix | string | required | one of: mix_with_reviews | after_reviews | before_reviews |
| interdayLearningMix | string | required | one of: mix_with_reviews | after_reviews | before_reviews |
| showTimer | boolean | required | |
| stopTimerOnAnswer | boolean | required | |
| scheduler | string | required | one of: fsrs | sm2 |
| desiredRetention | number | required | |
| hardMultiplier | number | required | |
| easyMultiplier | number | required | |
| intervalMultiplier | number | required | |
| lapseMultiplier | number | required | |
| minimumLapseIntervalDays | integer | required | |
| fsrsWeights | number[] | required | |
| easyDaysPercentages | number[] | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl -X DELETE "https://api.cardaga.in/deck-presets/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| ok | boolean | required | one of: true |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
| q | string | optional | at most 200 characters |
Request
curl "https://api.cardaga.in/note-types" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | required | 1–200 characters |
| kind | string | optional | one of: standard | cloze |
| css | string | optional | at most 200000 characters |
| latexPreamble | string | optional | at most 20000 characters |
| latexPostamble | string | optional | at most 20000 characters |
| sortFieldOrdinal | integer | optional | >= 0 |
| fields | object[] | required | |
| templates | object[] | required |
Request
curl -X POST "https://api.cardaga.in/note-types" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| name | string | required | |
| kind | string | required | one of: standard | cloze |
| css | string | required | |
| latexPreamble | string | required | |
| latexPostamble | string | required | |
| aiNotes | string | required | |
| sortFieldOrdinal | integer | required | >= 0 |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| fields | object[] | required | |
| templates | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/note-types/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| name | string | required | |
| kind | string | required | one of: standard | cloze |
| css | string | required | |
| latexPreamble | string | required | |
| latexPostamble | string | required | |
| aiNotes | string | required | |
| sortFieldOrdinal | integer | required | >= 0 |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| fields | object[] | required | |
| templates | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | optional | 1–200 characters |
| css | string | optional | at most 200000 characters |
| latexPreamble | string | optional | at most 20000 characters |
| latexPostamble | string | optional | at most 20000 characters |
| sortFieldOrdinal | integer | optional | >= 0 |
Request
curl -X PATCH "https://api.cardaga.in/note-types/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| name | string | required | |
| kind | string | required | one of: standard | cloze |
| css | string | required | |
| latexPreamble | string | required | |
| latexPostamble | string | required | |
| aiNotes | string | required | |
| sortFieldOrdinal | integer | required | >= 0 |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| fields | object[] | required | |
| templates | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| notes | string | optional | at most 2000 characters |
| fields | object[] | optional |
Request
curl -X PATCH "https://api.cardaga.in/note-types/{id}/guide" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| notes | string | required | at most 2000 characters |
| fields | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | required | 1–200 characters |
Request
curl -X POST "https://api.cardaga.in/note-types/{id}/clone" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| name | string | required | |
| kind | string | required | one of: standard | cloze |
| css | string | required | |
| latexPreamble | string | required | |
| latexPostamble | string | required | |
| aiNotes | string | required | |
| sortFieldOrdinal | integer | required | >= 0 |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| fields | object[] | required | |
| templates | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| fieldOps | unknown[] | required | |
| templateOps | unknown[] | required |
Request
curl -X POST "https://api.cardaga.in/note-types/{id}/migration-preview" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| notesAffected | integer | required | >= 0 |
| fieldsOrphaned | object[] | required | |
| cardsAdded | integer | required | >= 0 |
| cardsRemoved | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| requestId | string | required | 8–64 characters |
| acknowledgeOrphans | boolean | required | |
| expectedPreview | object | required | |
| fieldOps | unknown[] | required | |
| templateOps | unknown[] | required |
Request
curl -X POST "https://api.cardaga.in/note-types/{id}/migrate" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| noteType | object | required | |
| applied | object | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
| q | string | optional | at most 4000 characters |
| deckId | uuid | optional | |
| noteTypeId | uuid | optional | |
| tag | string | optional | at most 500 characters |
| flag | integer | null | optional | >= 0, <= 7 |
| state | string | optional | one of: new | learning | review | relearning |
| includeDeleted | unknown | optional | |
| sort | string | optional | one of: created | modified | sortField | noteType |
| direction | string | optional | one of: asc | desc |
Request
curl "https://api.cardaga.in/notes" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| noteTypeId | uuid | required | |
| deckId | uuid | required | |
| fields | object | required | |
| tags | string[] | optional | |
| guid | string | optional | 1–100 characters |
| requestId | string | optional | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/notes" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| noteTypeId | uuid | required | |
| guid | string | required | |
| fields | object | required | |
| tags | string[] | required | |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| deletedAt | date-time | null | required | |
| cardIds | uuid[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/notes/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| noteTypeId | uuid | required | |
| guid | string | required | |
| fields | object | required | |
| tags | string[] | required | |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| deletedAt | date-time | null | required | |
| cardIds | uuid[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| fields | object | optional | |
| tags | string[] | optional | |
| ifUnmodifiedSince | date-time | optional | |
| requestId | string | optional | 8–64 characters |
Request
curl -X PATCH "https://api.cardaga.in/notes/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| noteTypeId | uuid | required | |
| guid | string | required | |
| fields | object | required | |
| tags | string[] | required | |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| deletedAt | date-time | null | required | |
| cardIds | uuid[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| requestId | string | optional | 8–64 characters |
Request
curl -X DELETE "https://api.cardaga.in/notes/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| ok | boolean | required | one of: true |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| affected | integer | required | >= 0 |
| dryRun | boolean | required | |
| undoLabel | string | optional |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| noteTypeId | uuid | required | |
| firstField | string | required | at most 10000 characters |
Request
curl -X POST "https://api.cardaga.in/notes/duplicates" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| noteIds | uuid[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
| q | string | optional | at most 4000 characters |
| deckId | uuid | optional | |
| noteId | uuid | optional | |
| state | string | optional | one of: new | learning | review | relearning |
| flag | integer | null | optional | >= 0, <= 7 |
| suspended | unknown | optional | |
| buried | unknown | optional | |
| isLeech | unknown | optional | |
| dueBefore | date-time | optional | |
| sort | string | optional | one of: due | created | interval | lapses | reps | deck | position |
| direction | string | optional | one of: asc | desc |
Request
curl "https://api.cardaga.in/cards" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/cards/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| card | object | required | |
| noteId | uuid | required | |
| noteTypeId | uuid | required | |
| questionHtml | string | required | |
| answerHtml | string | required | |
| answerIncludesQuestion | boolean | required | |
| css | string | required | |
| tags | string[] | required | |
| deckDisplayPath | string | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cardIds | uuid[] | required | |
| requestId | string | required | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/cards/suspend" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| affected | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cardIds | uuid[] | required | |
| requestId | string | required | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/cards/unsuspend" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| affected | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cardIds | uuid[] | required | |
| requestId | string | required | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/cards/bury" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| affected | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cardIds | uuid[] | required | |
| requestId | string | required | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/cards/unbury" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| affected | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cardIds | uuid[] | required | |
| requestId | string | required | 8–64 characters |
| flag | integer | required | >= 0, <= 7 |
Request
curl -X POST "https://api.cardaga.in/cards/flag" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| affected | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cardIds | uuid[] | required | |
| requestId | string | required | 8–64 characters |
| startPosition | integer | required | >= 0 |
| step | integer | required | >= 0, <= 1000 |
| randomize | boolean | required | |
| shiftExisting | boolean | required |
Request
curl -X POST "https://api.cardaga.in/cards/reposition" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| affected | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cardIds | uuid[] | required | |
| requestId | string | required | 8–64 characters |
| targetDeckId | uuid | required |
Request
curl -X POST "https://api.cardaga.in/cards/move" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| affected | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
Request
curl "https://api.cardaga.in/card-skills" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | required | 1–200 characters |
| instructions | string | required | 1–8000 characters |
| criteria | string | required | 1–4000 characters |
| config | object | required |
Request
curl -X POST "https://api.cardaga.in/card-skills" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| name | string | required | 1–200 characters |
| instructions | string | required | 1–8000 characters |
| criteria | string | required | 1–4000 characters |
| config | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| deletedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/card-skills/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| name | string | required | 1–200 characters |
| instructions | string | required | 1–8000 characters |
| criteria | string | required | 1–4000 characters |
| config | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| deletedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | optional | 1–200 characters |
| instructions | string | optional | 1–8000 characters |
| criteria | string | optional | 1–4000 characters |
| config | object | optional |
Request
curl -X PATCH "https://api.cardaga.in/card-skills/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| name | string | required | 1–200 characters |
| instructions | string | required | 1–8000 characters |
| criteria | string | required | 1–4000 characters |
| config | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| deletedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl -X DELETE "https://api.cardaga.in/card-skills/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| ok | boolean | required | one of: true |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| voices | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
Request
curl "https://api.cardaga.in/skill-runs" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| skillId | uuid | required | |
| deckId | uuid | optional | |
| noteIds | uuid[] | optional |
Request
curl -X POST "https://api.cardaga.in/skill-runs" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| skillId | uuid | required | |
| deckId | uuid | null | required | |
| noteIds | uuid[] | required | |
| status | string | required | one of: queued | running | succeeded | failed | cancelled |
| progressPct | integer | required | >= 0, <= 100 |
| totals | object | required | |
| report | object[] | required | |
| error | string | null | required | |
| stale | boolean | required | |
| createdAt | date-time | required | |
| finishedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/skill-runs/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| skillId | uuid | required | |
| deckId | uuid | null | required | |
| noteIds | uuid[] | required | |
| status | string | required | one of: queued | running | succeeded | failed | cancelled |
| progressPct | integer | required | >= 0, <= 100 |
| totals | object | required | |
| report | object[] | required | |
| error | string | null | required | |
| stale | boolean | required | |
| createdAt | date-time | required | |
| finishedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| requestId | string | required | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/skill-runs/{id}/cancel" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| skillId | uuid | required | |
| deckId | uuid | null | required | |
| noteIds | uuid[] | required | |
| status | string | required | one of: queued | running | succeeded | failed | cancelled |
| progressPct | integer | required | >= 0, <= 100 |
| totals | object | required | |
| report | object[] | required | |
| error | string | null | required | |
| stale | boolean | required | |
| createdAt | date-time | required | |
| finishedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| requestId | string | required | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/skill-runs/{id}/undo" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| skillId | uuid | required | |
| deckId | uuid | null | required | |
| noteIds | uuid[] | required | |
| status | string | required | one of: queued | running | succeeded | failed | cancelled |
| progressPct | integer | required | >= 0, <= 100 |
| totals | object | required | |
| report | object[] | required | |
| error | string | null | required | |
| stale | boolean | required | |
| createdAt | date-time | required | |
| finishedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| deckId | uuid | required | |
| limit | integer | optional | >= 1, <= 50 |
| lastAnsweredCardId | uuid | optional | |
| sessionAnchor | string | optional |
Request
curl "https://api.cardaga.in/queue/next" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| cards | object[] | required | |
| counts | object | required | |
| preset | object | required | |
| showTimer | boolean | optional | |
| stopTimerOnAnswer | boolean | optional | |
| learnAheadMins | integer | optional | >= 0 |
| scheduler | string | required | one of: fsrs | sm2 |
| presetByDeck | object | optional | |
| balance | object | optional | |
| serverNow | date-time | required | |
| nextRolloverAt | date-time | required | |
| nextReadyAt | date-time | null | optional | |
| sessionAnchor | string | optional |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cardId | uuid | required | |
| rating | string | required | one of: again | hard | good | easy |
| durationMs | integer | required | >= 0, <= 3600000 |
| clientEventId | string | required | 8–64 characters |
| answeredAt | date-time | required |
Request
curl -X POST "https://api.cardaga.in/queue/answer" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| replayed | boolean | required | |
| card | object | required | |
| review | object | required | |
| counts | object | required | |
| undo | object | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| expectedReviewLogId | uuid | optional |
Request
curl -X POST "https://api.cardaga.in/queue/undo" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| kind | string | required | |
| label | string | required | |
| counts | object | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
| cardId | uuid | optional | |
| from | date-time | optional | |
| to | date-time | optional |
Request
curl "https://api.cardaga.in/reviews" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| timezone | string | required | at least 1 characters |
| rolloverHour | integer | required | >= 0, <= 23 |
| learnAheadMins | integer | required | >= 0 |
| theme | string | required | |
| scheduler | string | required | one of: fsrs | sm2 |
| loadBalancerEnabled | boolean | required | |
| newCardsIgnoreReviewLimit | boolean | required | |
| applyAllParentLimits | boolean | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| timezone | string | optional | 1–64 characters |
| rolloverHour | integer | optional | >= 0, <= 23 |
| learnAheadMins | integer | optional | >= 0, <= 1440 |
Request
curl -X PATCH "https://api.cardaga.in/preferences" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| timezone | string | required | at least 1 characters |
| rolloverHour | integer | required | >= 0, <= 23 |
| learnAheadMins | integer | required | >= 0 |
| theme | string | required | |
| scheduler | string | required | one of: fsrs | sm2 |
| loadBalancerEnabled | boolean | required | |
| newCardsIgnoreReviewLimit | boolean | required | |
| applyAllParentLimits | boolean | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| prefix | string | required | at most 200 characters |
| limit | integer | optional | >= 1, <= 50 |
Request
curl "https://api.cardaga.in/search/suggest" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| suggestions | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| deckId | uuid | optional |
Request
curl "https://api.cardaga.in/stats/summary" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| totalCards | integer | required | >= 0 |
| newCards | integer | required | >= 0 |
| learningCards | integer | required | >= 0 |
| reviewCards | integer | required | >= 0 |
| relearningCards | integer | required | >= 0 |
| suspendedCards | integer | required | >= 0 |
| buriedCards | integer | required | >= 0 |
| matureCards | integer | required | >= 0 |
| reviewsToday | integer | required | >= 0 |
| millisecondsToday | integer | required | >= 0 |
| studiedDays | integer | required | >= 0 |
| currentStreakDays | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| from | string | required | |
| to | string | required | |
| deckId | uuid | optional |
Request
curl "https://api.cardaga.in/stats/heatmap" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| days | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| days | integer | optional | >= 1, <= 365 |
| deckId | uuid | optional | |
| includeSuspended | boolean | optional |
Request
curl "https://api.cardaga.in/stats/forecast" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| days | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| deckId | uuid | optional |
Request
curl "https://api.cardaga.in/stats/intervals" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| buckets | object[] | required | |
| averageDays | number | required | >= 0 |
| medianDays | number | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| from | string | required | |
| to | string | required | |
| deckId | uuid | optional |
Request
curl "https://api.cardaga.in/stats/buttons" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| rows | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| deckId | uuid | optional |
Request
curl "https://api.cardaga.in/stats/true-retention" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| windows | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| days | integer | optional | >= 1, <= 90 |
| deckId | uuid | optional |
Request
curl "https://api.cardaga.in/stats/memory" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| cards | integer | required | >= 0 |
| recall | number[] | required | |
| dueBy | integer[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| decks | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| days | integer | optional | >= 7, <= 365 |
Request
curl "https://api.cardaga.in/stats/time-sinks" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| totalMs | integer | required | >= 0 |
| cardsReviewed | integer | required | >= 0 |
| curve | object[] | required | |
| top | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cardId | uuid | required |
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
| cardId | uuid | optional | |
| rootsOnly | unknown | optional | |
| threadRootId | uuid | optional | |
| pinnedOnly | unknown | optional |
Request
curl "https://api.cardaga.in/cards/{cardId}/insights" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cardId | uuid | required | |
| prompt | string | required | 1–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 /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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| prompt | string | required | 1–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 /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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| isPinned | boolean | required |
Request
curl -X PATCH "https://api.cardaga.in/insights/{id}/pin" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| cardId | uuid | required | |
| threadRootId | uuid | null | required | |
| parentId | uuid | null | required | |
| role | string | required | one of: user | assistant | action |
| contentMd | string | required | |
| summary | string | null | required | |
| isPinned | boolean | required | |
| model | string | null | required | |
| promptVersion | string | null | required | |
| contextDigest | object | null | required | |
| plan | object | null | required | |
| feedback | string | null | optional | one of: up | down |
| version | integer | required | > 0 |
| createdAt | date-time | required | |
| deletedAt | date-time | null | required | |
| action | object | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| feedback | string | null | required | one of: up | down |
Request
curl -X PATCH "https://api.cardaga.in/insights/{id}/feedback" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| cardId | uuid | required | |
| threadRootId | uuid | null | required | |
| parentId | uuid | null | required | |
| role | string | required | one of: user | assistant | action |
| contentMd | string | required | |
| summary | string | null | required | |
| isPinned | boolean | required | |
| model | string | null | required | |
| promptVersion | string | null | required | |
| contextDigest | object | null | required | |
| plan | object | null | required | |
| feedback | string | null | optional | one of: up | down |
| version | integer | required | > 0 |
| createdAt | date-time | required | |
| deletedAt | date-time | null | required | |
| action | object | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl -X DELETE "https://api.cardaga.in/insights/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| ok | boolean | required | one of: true |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| noteTypeId | uuid | required | |
| deckId | uuid | required | |
| fields | object | required | |
| tags | string[] | optional | |
| requestId | string | optional | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/insights/{id}/promote" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| noteTypeId | uuid | required | |
| guid | string | required | |
| fields | object | required | |
| tags | string[] | required | |
| sourceSystem | string | required | one of: cardagain | anki |
| sourceId | string | null | required | |
| metadata | object | required | |
| createdAt | date-time | required | |
| updatedAt | date-time | required | |
| deletedAt | date-time | null | required | |
| cardIds | uuid[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
| noteId | uuid | optional | |
| deckId | uuid | optional | |
| noteInDeckId | uuid | optional | |
| status | string | optional | one of: proposed | applied | rejected | reverted |
| actor | string | optional | one of: user | scribe | import |
| runId | uuid | optional |
Request
curl "https://api.cardaga.in/revisions" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/revisions/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| noteId | uuid | null | required | |
| deckId | uuid | null | required | |
| noteTypeId | uuid | null | required | |
| status | string | required | one of: proposed | applied | rejected | reverted |
| actor | string | required | one of: user | scribe | import |
| toolName | string | null | required | |
| requestId | string | required | |
| diff | object[] | required | |
| beforeSnapshot | object | required | |
| appliedAt | date-time | null | required | |
| revertedAt | date-time | null | required | |
| createdAt | date-time | required | |
| runId | uuid | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| requestId | string | required | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/revisions/{id}/accept" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| noteId | uuid | null | required | |
| deckId | uuid | null | required | |
| noteTypeId | uuid | null | required | |
| status | string | required | one of: proposed | applied | rejected | reverted |
| actor | string | required | one of: user | scribe | import |
| toolName | string | null | required | |
| requestId | string | required | |
| diff | object[] | required | |
| beforeSnapshot | object | required | |
| appliedAt | date-time | null | required | |
| revertedAt | date-time | null | required | |
| createdAt | date-time | required | |
| runId | uuid | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| requestId | string | required | 8–64 characters |
| reason | string | optional | at most 2000 characters |
Request
curl -X POST "https://api.cardaga.in/revisions/{id}/reject" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| noteId | uuid | null | required | |
| deckId | uuid | null | required | |
| noteTypeId | uuid | null | required | |
| status | string | required | one of: proposed | applied | rejected | reverted |
| actor | string | required | one of: user | scribe | import |
| toolName | string | null | required | |
| requestId | string | required | |
| diff | object[] | required | |
| beforeSnapshot | object | required | |
| appliedAt | date-time | null | required | |
| revertedAt | date-time | null | required | |
| createdAt | date-time | required | |
| runId | uuid | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| requestId | string | required | 8–64 characters |
| verdict | string | required | one of: accept | reject |
| ids | uuid[] | required |
Request
curl -X POST "https://api.cardaga.in/revisions/decide" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| decided | uuid[] | required | |
| decidedCards | integer | required | >= 0 |
| refused | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| requestId | string | required | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/revisions/{id}/revert" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| noteId | uuid | null | required | |
| deckId | uuid | null | required | |
| noteTypeId | uuid | null | required | |
| status | string | required | one of: proposed | applied | rejected | reverted |
| actor | string | required | one of: user | scribe | import |
| toolName | string | null | required | |
| requestId | string | required | |
| diff | object[] | required | |
| beforeSnapshot | object | required | |
| appliedAt | date-time | null | required | |
| revertedAt | date-time | null | required | |
| createdAt | date-time | required | |
| runId | uuid | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| requestId | string | required | 8–64 characters |
Request
curl -X POST "https://api.cardaga.in/revisions/{id}/reapply" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| noteId | uuid | null | required | |
| deckId | uuid | null | required | |
| noteTypeId | uuid | null | required | |
| status | string | required | one of: proposed | applied | rejected | reverted |
| actor | string | required | one of: user | scribe | import |
| toolName | string | null | required | |
| requestId | string | required | |
| diff | object[] | required | |
| beforeSnapshot | object | required | |
| appliedAt | date-time | null | required | |
| revertedAt | date-time | null | required | |
| createdAt | date-time | required | |
| runId | uuid | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| confirmation | string | required | 1–320 characters |
Request
curl -X DELETE "https://api.cardaga.in/account" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| string | required | ||
| removed | object | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
| status | string | optional | one of: running | awaiting_user | done | failed | cancelled |
Request
curl "https://api.cardaga.in/ingest/sessions" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| source | unknown | required | |
| instructions | string | required | at most 2000 characters |
| deckId | uuid | optional | |
| guidance | object | optional | |
| advancedStyling | boolean | optional |
Request
curl -X POST "https://api.cardaga.in/ingest/sessions" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
200 response body: none.
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
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 /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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| callId | string | required | at least 1 characters |
| result | object | required |
Request
curl -X POST "https://api.cardaga.in/ingest/sessions/{id}/tool-results" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| accepted | boolean | required | one of: true |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/ingest/sessions/{id}/document" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| document | object | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/ingest/sessions/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| status | string | required | one of: running | awaiting_user | done | failed | cancelled |
| source | unknown | required | |
| instructions | string | required | |
| deckId | uuid | null | required | |
| guidance | object | optional | |
| pendingQuestion | unknown | null | required | |
| steps | object | required | |
| error | string | null | required | |
| feedback | string | null | optional | one of: up | down |
| createdAt | date-time | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl -X POST "https://api.cardaga.in/ingest/sessions/{id}/cancel" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| status | string | required | one of: running | awaiting_user | done | failed | cancelled |
| source | unknown | required | |
| instructions | string | required | |
| deckId | uuid | null | required | |
| guidance | object | optional | |
| pendingQuestion | unknown | null | required | |
| steps | object | required | |
| error | string | null | required | |
| feedback | string | null | optional | one of: up | down |
| createdAt | date-time | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| feedback | string | null | required | one of: up | down |
Request
curl -X PATCH "https://api.cardaga.in/ingest/sessions/{id}/feedback" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| status | string | required | one of: running | awaiting_user | done | failed | cancelled |
| source | unknown | required | |
| instructions | string | required | |
| deckId | uuid | null | required | |
| guidance | object | optional | |
| pendingQuestion | unknown | null | required | |
| steps | object | required | |
| error | string | null | required | |
| feedback | string | null | optional | one of: up | down |
| createdAt | date-time | required | |
| updatedAt | date-time | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| cursor | string | optional | at least 1 characters |
| limit | integer | optional | >= 1, <= 200 |
| status | string | optional | one of: queued | running | succeeded | failed | cancelled |
Request
curl "https://api.cardaga.in/imports" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| items | object[] | required | |
| nextCursor | string | null | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| filename | string | required | |
| sizeBytes | integer | required | >= 0 |
| status | string | required | one of: queued | running | succeeded | failed | cancelled |
| progressPct | integer | required | >= 0, <= 100 |
| stage | string | null | required | |
| report | object | null | required | |
| error | string | null | required | |
| createdAt | date-time | required | |
| finishedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| maxArchiveBytes | integer | null | required | > 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/imports/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| filename | string | required | |
| sizeBytes | integer | required | >= 0 |
| status | string | required | one of: queued | running | succeeded | failed | cancelled |
| progressPct | integer | required | >= 0, <= 100 |
| stage | string | null | required | |
| report | object | null | required | |
| error | string | null | required | |
| createdAt | date-time | required | |
| finishedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/imports/{id}/report" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| counts | object | required | |
| noteTypesMapped | object[] | required | |
| mediaResolved | integer | required | >= 0 |
| mediaMissing | string[] | required | |
| templatesWithFallback | object[] | required | |
| skipped | object[] | required | |
| timezoneCheck | object | optional | |
| scheduler | string | optional | one of: fsrs | sm2 |
| presetsAdjusted | object[] | optional |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| deckIds | uuid[] | optional | |
| includeMedia | boolean | required | |
| includeScheduling | boolean | required | |
| includeDeckPresets | boolean | optional | |
| includeAiNotes | boolean | optional | |
| legacySupport | boolean | optional | |
| format | string | optional | one of: apkg | colpkg |
Request
curl -X POST "https://api.cardaga.in/exports" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
201
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| status | string | required | one of: queued | running | succeeded | failed | cancelled |
| progressPct | integer | required | >= 0, <= 100 |
| filename | string | null | required | |
| sizeBytes | integer | null | required | >= 0 |
| error | string | null | required | |
| createdAt | date-time | required | |
| finishedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/exports/{id}" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required | |
| status | string | required | one of: queued | running | succeeded | failed | cancelled |
| progressPct | integer | required | >= 0, <= 100 |
| filename | string | null | required | |
| sizeBytes | integer | null | required | >= 0 |
| error | string | null | required | |
| createdAt | date-time | required | |
| finishedAt | date-time | null | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| id | uuid | required |
Request
curl "https://api.cardaga.in/exports/{id}/download" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
200 response body: none.
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
| Field | Type | Required | Notes |
|---|---|---|---|
| protocolVersion | integer | required | > 0 |
| migrationHash | string | required | at least 1 characters |
| scm | date-time | required | |
| maxUsn | integer | required | >= 0 |
| mediaMaxUsn | integer | required | >= 0 |
| fullSyncRequired | boolean | required | |
| serverNow | date-time | optional |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| graves | object[] | required | |
| after | integer | required | >= 0 |
Request
curl -X POST "https://api.cardaga.in/sync/graves" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| graves | object[] | required | |
| assigned | object[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| entity | string | required | one 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 |
| after | integer | null | required | >= 0 |
| limit | integer | optional | >= 1, <= 1000 |
Request
curl "https://api.cardaga.in/sync/changes" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| rows | object[] | required | |
| nextAfter | integer | required | >= 0 |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| entity | string | required | one 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 |
| rows | object[] | required |
Request
curl -X POST "https://api.cardaga.in/sync/changes" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| assigned | object[] | required | |
| conflicted | object[] | optional |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| summary | object | required |
Request
curl -X POST "https://api.cardaga.in/sync/finish" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| ok | boolean | required | |
| maxUsn | integer | required | >= 0 |
| fullSyncRequired | boolean | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| direction | string | required | one of: upload | download |
| uploadId | uuid | optional |
Request
curl -X POST "https://api.cardaga.in/sync/full" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
202
| Field | Type | Required | Notes |
|---|---|---|---|
| jobId | uuid | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| sha256s | string[] | required |
Request
curl -X POST "https://api.cardaga.in/sync/media/wanted" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
| Field | Type | Required | Notes |
|---|---|---|---|
| wanted | string[] | required |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| userId | uuid | required | |
| string | required | at least 1 characters |
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
| Field | Type | Required | Notes |
|---|---|---|---|
| role | string | required | one of: hub | spoke |
| peerConfigured | boolean | required | |
| pendingRows | integer | required | >= 0 |
| pendingGraves | integer | required | >= 0 |
| lastSyncAt | date-time | null | required | |
| fullSyncRequired | boolean | required |
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 /.well-known/assetlinks.json
Android's Digital Asset Links file for app-link verification.
Lists the SHA-256 certificate fingerprints of the Android app this deployment vouches for, so the OS grants it the reset-password and verify-email links instead of opening them in a browser. Values come from this deployment's own configuration.
Request
curl "https://api.cardaga.in/.well-known/assetlinks.json" \
-H "Authorization: Bearer $CARDAGAIN_API_KEY"Responses
200
200 response body: none.
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
| Field | Type | Required | Notes |
|---|---|---|---|
| applinks | object | required |