Anki Sync Integration

Overview

This document describes the Anki integration for the Immersive StudyEvent + UserAbility system, including metadata format and API contract for bi-directional sync.

Metadata Format

Immersive Metadata Field

Each card exported from Immersive to Anki should include hidden metadata identifying the underlying resources.

Recommended Implementation: Hidden <div> element with data-immersive-meta attribute

JSON Schema

{
  "resources": [
    {"type": "Infinitive", "id": 45},
    {"type": "Conjugation", "id": 67},
    {"type": "Sentence", "id": 123}
  ],
  "card_template": "verb_conjugation_with_audio_v1",
  "deck_id": 42,
  "exported_at": "2025-12-04T10:23:00Z",
  "version": 1
}

Field Descriptions

  • resources (array, required): List of all Immersive resources on this card
    • type: “Sentence”, “Conjugation”, “VerbLemma”, “GrammarNote”, “Infinitive”
    • id: Resource ID in Immersive database
  • card_template (string, required): Identifies card presentation format

  • deck_id (integer, optional): Immersive deck ID if from a specific deck

  • exported_at (ISO 8601 datetime, required): When card was exported

  • version (integer, required): Metadata format version (currently 1)

HTML Implementation Example

<div class="card-front">
  <p>Conjuguez: <strong></strong></p>
  <p> </p>
</div>

<div class="card-back">
  <p><strong></strong></p>
  <p></p>
  <audio controls><source src=""></audio>
</div>

<!-- Hidden metadata -->
<div style="display:none;" 
  data-immersive-meta='{"resources":[{"type":"Infinitive","id":45},{"type":"Conjugation","id":67},{"type":"Sentence","id":123}],"card_template":"verb_conjugation_with_audio_v1","deck_id":42,"exported_at":"2025-12-04T10:23:00Z","version":1}'>
</div>

API Contract

Authentication

API requests require user authentication via:

  • Header: Authorization: Bearer <token>
  • Cookie-based session authentication

Endpoint: Create Study Events

POST /api/v1/study_events

Single Resource Request

{
  "resource_type": "Sentence",
  "resource_id": 123,
  "external_card_id": "1234567890",
  "source": "anki_desktop",
  "event_type": "review",
  "modality": "read",
  "happened_at": "2025-12-04T10:23:00Z",
  "ease_button": 3,
  "interval_days": 7,
  "previous_interval_days": 3,
  "ease_factor": 2300,
  "review_duration_ms": 4200,
  "review_type": "review",
  "total_reviews": 10,
  "lapses": 1,
  "deck_name": "French Verbs"
}

Multi-Resource Request

{
  "resources": [
    {"resource_type": "Infinitive", "resource_id": 45},
    {"resource_type": "Conjugation", "resource_id": 67},
    {"resource_type": "Sentence", "resource_id": 123}
  ],
  "external_card_id": "1234567890",
  "source": "anki_desktop",
  "event_type": "review",
  "modality": "read",
  "happened_at": "2025-12-04T10:23:00Z",
  "ease_button": 3,
  "interval_days": 7,
  "ease_factor": 2300,
  "review_type": "review",
  "total_reviews": 10,
  "lapses": 1,
  "deck_name": "French Verbs"
}

Field Descriptions

Required:

  • resource_type OR resources - Resource identifier(s)
  • resource_id (when using single resource)
  • event_type - One of: view, play_audio, review, reveal_answer, mark_correct, mark_wrong, suspend
  • source - One of: anki_desktop, anki_mobile, immersive_web, immersive_mobile, api

Optional:

  • modality - One of: read, write, listen, speak (default: read)
  • external_card_id - Anki card ID
  • happened_at - ISO 8601 datetime (defaults to current time)
  • ease_button - 1-4 (1=again, 2=hard, 3=good, 4=easy)
  • interval_days - Current interval in days
  • interval_seconds - Current interval in seconds (for intraday learning)
  • previous_interval_days - Previous interval value
  • ease_factor - Ease factor in permille (e.g., 2500 = 250%)
  • review_duration_ms - Review duration in milliseconds (max 60000)
  • review_type - Type: learn, review, relearn, filtered, manual, rescheduled
  • total_reviews - Total number of reviews
  • lapses - Number of times marked wrong
  • deck_name - Name of the deck
  • metadata - Additional custom data (JSONB)
  • raw_provider_data - Provider-specific data (JSONB)

Response

Success (201 Created):

{
  "success": true,
  "events_created": 3,
  "ids": [456, 457, 458],
  "happened_at": "2025-12-04T10:23:00Z"
}

Error (404 Not Found):

{
  "error": "No valid resources found"
}

Error (400 Bad Request):

{
  "error": "Invalid event_type"
}

Anki Review Log Field Mapping

Anki revlog Table Structure

CREATE TABLE revlog (
    id              integer primary key,  -- epoch-ms timestamp
    cid             integer not null,      -- card ID
    usn             integer not null,      -- update sequence number
    ease            integer not null,      -- button pressed (1-4)
    ivl             integer not null,      -- interval (negative=seconds, positive=days)
    lastIvl         integer not null,      -- previous interval
    factor          integer not null,      -- ease factor in permille
    time            integer not null,      -- review duration in ms (max 60000)
    type            integer not null       -- review type (0-5)
);

Field Mapping: Anki → Immersive

Anki Field Immersive Field Notes
id happened_at Convert: Time.at(id / 1000.0)
cid external_card_id Store as string
ease ease_button 1-4 directly
ease event_type 1→mark_wrong, 2-4→mark_correct or review
ivl (positive) interval_days Store positive values
ivl (negative) interval_seconds Convert to positive: ivl.abs
lastIvl previous_interval_days Track progression
factor ease_factor Permille (2500 = 250%)
time review_duration_ms Max 60000
type review_type 0→learn, 1→review, 2→relearn, 3→filtered, 4→manual, 5→rescheduled

Cards Table Fields

Anki Field Immersive Field Notes
reps total_reviews Total reviews count
lapses lapses Times marked wrong
Deck name deck_name Resolved by plugin

Anki Plugin Implementation Guide

Workflow

  1. Card Display: Parse data-immersive-meta from card HTML
  2. Review Event: Capture review outcome and POST to API
  3. Multi-Resource: Extract all resources from metadata, send in single request
  4. Audio Play: Optional event tracking for audio interactions
  5. Batch Sync: Queue events locally, batch POST periodically

Example Python Code

import json
import requests
from datetime import datetime

class ImmersiveSync:
    def __init__(self, api_token, base_url="https://immersive-app.com"):
        self.api_token = api_token
        self.base_url = base_url
    
    def parse_card_metadata(self, card):
        """Extract Immersive metadata from card."""
        note = card.note()
        for field in note.fields:
            if 'data-immersive-meta' in field:
                import re
                match = re.search(r"data-immersive-meta='([^']+)'", field)
                if match:
                    return json.loads(match.group(1))
        return None
    
    def log_review(self, card, revlog_entry):
        """Send multi-resource review event to Immersive."""
        meta = self.parse_card_metadata(card)
        if not meta or not meta.get('resources'):
            return
        
        # Map Anki type to review_type string
        type_map = {
            0: "learn", 1: "review", 2: "relearn",
            3: "filtered", 4: "manual", 5: "rescheduled"
        }
        
        # Parse interval
        ivl = revlog_entry['ivl']
        interval_days = ivl if ivl >= 0 else None
        interval_seconds = abs(ivl) if ivl < 0 else None
        
        event_data = {
            "resources": meta['resources'],
            "external_card_id": str(card.id),
            "source": "anki_desktop",
            "event_type": "review",
            "modality": "read",  # Could be determined from card template
            "happened_at": datetime.fromtimestamp(
                revlog_entry['id'] / 1000.0
            ).isoformat(),
            "ease_button": revlog_entry['ease'],
            "interval_days": interval_days,
            "interval_seconds": interval_seconds,
            "previous_interval_days": revlog_entry.get('lastIvl'),
            "ease_factor": revlog_entry['factor'],
            "review_duration_ms": revlog_entry['time'],
            "review_type": type_map.get(revlog_entry['type'], "unknown"),
            "total_reviews": card.reps,
            "lapses": card.lapses,
            "deck_name": card.col.decks.name(card.did)
        }
        
        try:
            response = requests.post(
                f"{self.base_url}/api/v1/study_events",
                json=event_data,
                headers={"Authorization": f"Bearer {self.api_token}"},
                timeout=5
            )
            response.raise_for_status()
            result = response.json()
            print(f"Synced {result['events_created']} events")
        except Exception as e:
            print(f"Failed to sync: {e}")
            # Queue for retry

Modality Detection

The plugin can infer modality from card template or user configuration:

def detect_modality(card_template):
    """Infer modality from card template name."""
    template_lower = card_template.lower()
    
    if 'audio' in template_lower or 'listening' in template_lower:
        return "listen"
    elif 'speaking' in template_lower or 'pronunciation' in template_lower:
        return "speak"
    elif 'writing' in template_lower or 'production' in template_lower:
        return "write"
    else:
        return "read"  # Default

Testing

Manual Testing with cURL

curl -X POST https://immersive-app.com/api/v1/study_events \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "resources": [
      {"resource_type": "Infinitive", "resource_id": 45},
      {"resource_type": "Conjugation", "resource_id": 67},
      {"resource_type": "Sentence", "resource_id": 123}
    ],
    "external_card_id": "1234567890",
    "source": "anki_desktop",
    "event_type": "review",
    "ease_button": 3,
    "interval_days": 7
  }'

Future Enhancements

Bi-Directional Card Sync

  • GET /api/v1/cards/changed_since?timestamp=<ISO8601>
  • Returns cards updated in Immersive since timestamp
  • Plugin updates local Anki cards

Conflict Resolution

  • “Latest change wins” strategy by default
  • Option to preserve local Anki changes with manual merge

Support

For questions:

  • Review the API spec examples
  • Check the Python implementation code
  • Open GitHub issues for bugs or feature requests

This site uses Just the Docs, a documentation theme for Jekyll.