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 cardtype: “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 (currently1)
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_typeORresources- Resource identifier(s)resource_id(when using single resource)event_type- One of:view,play_audio,review,reveal_answer,mark_correct,mark_wrong,suspendsource- 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 IDhappened_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 daysinterval_seconds- Current interval in seconds (for intraday learning)previous_interval_days- Previous interval valueease_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,rescheduledtotal_reviews- Total number of reviewslapses- Number of times marked wrongdeck_name- Name of the deckmetadata- 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
- Card Display: Parse
data-immersive-metafrom card HTML - Review Event: Capture review outcome and POST to API
- Multi-Resource: Extract all resources from metadata, send in single request
- Audio Play: Optional event tracking for audio interactions
- 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