Ingestion Sources API
March 8, 2026 ยท View on GitHub
Ingestion Sources provides a generic sync/import API for filesystem-backed content. A source can point at a local directory or a staged archive snapshot and sync into either the media sink or the notes sink.
This API is ingest-only. It defines sources, stages archive refreshes, and enqueues sync jobs. It does not currently ship a dedicated WebUI.
See Docs/Plans/2026-03-08-ingestion-source-sync-design.md for the original design and tradeoffs.
Endpoints
POST /api/v1/ingestion-sources- create a sourceGET /api/v1/ingestion-sources- list sourcesGET /api/v1/ingestion-sources/{source_id}- get a sourcePATCH /api/v1/ingestion-sources/{source_id}- update mutable source settingsPOST /api/v1/ingestion-sources/{source_id}/sync- enqueue a manual syncPOST /api/v1/ingestion-sources/{source_id}/archive- upload a new archive snapshot and enqueue syncGET /api/v1/ingestion-sources/{source_id}/items- inspect tracked source itemsPOST /api/v1/ingestion-sources/{source_id}/items/{item_id}/reattach- reattach a detached notes item
Core object: IngestionSource
Key fields:
id,user_idsource_type:local_directoryorarchive_snapshotsink_type:mediaornotespolicy:canonicalorimport_onlyenabledschedule_enabled,schedule_configconfigactive_job_idlast_successful_snapshot_idlast_sync_started_at,last_sync_completed_at,last_sync_status,last_errorlast_successful_sync_summary: summary of the last successful run, including counters likeprocessed,degraded_items,sink_failed_items,ingestion_failed_items,detached_conflictscreated_at,updated_at
Source types
local_directory
config requires:
{
"path": "/absolute/path/under/an/allowed/root"
}
Notes:
- Paths must resolve under
INGESTION_SOURCE_ALLOWED_ROOTS. - V1 supports manual sync and optional scheduled rescans.
- Local directory sources are intended for self-hosted/admin-controlled deployments where workers can access the configured path.
archive_snapshot
config is currently empty on create:
{}
Refreshes are driven by POST /api/v1/ingestion-sources/{source_id}/archive.
Supported archive upload formats:
.zip.tar.tar.gz,.tgz.tar.bz2,.tbz2.tar.xz,.txz
Safety rules:
- path traversal members are rejected
- symlinks are rejected
- encrypted ZIPs are rejected
Sink types
media
Use for broader document ingestion. Mixed sources can include Markdown, text, HTML, JSON, XML, DOCX, RTF, PDF, and EPUB. Changed items update the existing bound media/document record when possible.
notes
Use for text-first note sync. Notes are latest-state sync in v1 and do not preserve full historical note bodies by themselves.
If a sync-managed note is edited locally and diverges, the item becomes conflict_detached instead of being overwritten on the next sync.
Create
POST /api/v1/ingestion-sources
Example request for a local-directory notes source:
{
"source_type": "local_directory",
"sink_type": "notes",
"policy": "canonical",
"enabled": true,
"schedule_enabled": false,
"schedule": {},
"config": {
"path": "/srv/tldw/notes"
}
}
Example response:
{
"id": 12,
"user_id": 1,
"source_type": "local_directory",
"sink_type": "notes",
"policy": "canonical",
"enabled": true,
"schedule_enabled": false,
"schedule_config": {},
"config": {
"path": "/srv/tldw/notes"
},
"active_job_id": null,
"last_successful_snapshot_id": null,
"last_sync_started_at": null,
"last_sync_completed_at": null,
"last_sync_status": null,
"last_error": null,
"last_successful_sync_summary": {},
"created_at": "2026-03-08 12:00:00",
"updated_at": "2026-03-08 12:00:00"
}
List
GET /api/v1/ingestion-sources
Returns all sources owned by the authenticated user.
Example response:
[
{
"id": 12,
"user_id": 1,
"source_type": "archive_snapshot",
"sink_type": "media",
"policy": "canonical",
"enabled": true,
"schedule_enabled": false,
"schedule_config": {},
"config": {},
"active_job_id": null,
"last_successful_snapshot_id": 44,
"last_sync_started_at": "2026-03-08 12:05:00",
"last_sync_completed_at": "2026-03-08 12:05:03",
"last_sync_status": "success",
"last_error": null,
"last_successful_sync_summary": {
"processed": 8,
"created": 2,
"changed": 1,
"deleted": 0,
"unchanged": 5,
"degraded_items": 1,
"sink_failed_items": 1,
"ingestion_failed_items": 0,
"detached_conflicts": 0,
"current_item_count": 8
}
}
]
Get
GET /api/v1/ingestion-sources/{source_id}
Returns the same object shape as list, including last_successful_sync_summary.
Update
PATCH /api/v1/ingestion-sources/{source_id}
Mutable fields:
policyenabledschedule_enabledschedule
Example request:
{
"policy": "import_only",
"schedule_enabled": true,
"schedule": {
"cron": "0 * * * *",
"timezone": "UTC"
}
}
Important constraints:
source_type,sink_type, and source identity are immutable after the first successful sync.- Attempts to change immutable fields after first success return
409.
Manual sync
POST /api/v1/ingestion-sources/{source_id}/sync
Response:
{
"status": "queued",
"source_id": 12,
"job_id": 301
}
This only enqueues work. Sync execution runs through the Jobs worker path.
Archive refresh
POST /api/v1/ingestion-sources/{source_id}/archive
Multipart form field:
archive: ZIP or tar-family upload
Response:
{
"status": "queued",
"source_id": 12,
"job_id": 302,
"snapshot_status": "staged"
}
Notes:
- archive upload is only valid for
archive_snapshotsources - the uploaded archive is persisted as a source artifact
- a failed candidate refresh does not replace the prior successful snapshot
Items
GET /api/v1/ingestion-sources/{source_id}/items
Returns tracked items for the source.
Example response:
[
{
"id": 77,
"source_id": 12,
"normalized_relative_path": "alpha.md",
"content_hash": "b1946ac92492d2347c6235b4d2611184",
"sync_status": "sync_managed",
"binding": {
"note_id": "note-123",
"sync_status": "sync_managed",
"current_version": 4
},
"present_in_source": true
}
]
Common sync_status values:
sync_managedactiveconflict_detacheddegraded_ingestion_errordegraded_sink_errorarchived
Reattach
POST /api/v1/ingestion-sources/{source_id}/items/{item_id}/reattach
This is only supported for notes sinks and only when the item is currently conflict_detached.
It restores the binding to sync_managed and clears the tracked content hash so the next sync reapplies upstream content.
Failure model
Sync runs are source-scoped, but failures are isolated per item where possible:
- extraction/parsing failure marks an item
degraded_ingestion_error - sink apply failure marks an item
degraded_sink_error - notes divergence marks an item
conflict_detached
Successful items in the same run still apply, and the last successful snapshot summary records the degraded counters.
Current limitations
- No dedicated WebUI yet.
- Archive refresh is snapshot-based; archive item rename continuity is still
delete + create. - Notes sync is latest-state sync, not full note-history preservation.
- The public API currently exposes source and item state, but not a dedicated job-status endpoint for ingestion sources beyond the shared Jobs system.