Install the app
Download Ink & Echo and import a local file before wiring providers.
Documentation
Ink & Echo can import local media, connect to supported services, and accept provider JSON for metadata and source search. This page documents the JSON shape the app actually accepts today, with examples you can adapt for your own lawful sources.
Provider JSON
kind: "metadata"kind: "search"Start
Workflow
Import
{
"kind": "metadata",
"id": "my-provider",
"name": "My Provider",
"description": "Catalog metadata for my private library.",
"trustLabel": "Private",
"lawfulNote": "Only use this with catalogs you are allowed to access.",
"capabilities": {
"search": true
},
"search": {
"request": {
"method": "GET",
"url": "https://api.example.com/books?q={QUERY}",
"headers": {
"Accept": "application/json"
},
"timeout": 15000
},
"response": {
"type": "json",
"resultsPath": "items",
"mapping": {
"title": "title",
"author": "author"
}
}
}
}Metadata
A metadata provider must include a display name and at least one data source: staticentries, remote search, remotediscover sections, or static results.
| Field | Type | Description |
|---|---|---|
| kind* | "metadata" | Declares this file as a metadata provider. |
| id | string | Stable identifier. If omitted, Ink & Echo generates one from the provider name. |
| name or label* | string | Display name shown in Settings and result details. |
| version | string | Optional version string for your own tracking. |
| description | string | Short summary shown in the provider review step. |
| trustLabel | string | Example: Built In, User Added, Private API, Self Hosted. |
| lawfulNote | string | Required in practice for clarity. Tell users what the provider is allowed to access. |
| capabilities | object or string[] | Enabled capability names. Truthy object keys become capabilities. |
| search | object | Remote JSON search definition. |
| discover | object | Optional discovery sections for the Discover screen. |
| entries | array | Static metadata records bundled directly in the JSON. |
| rateLimit | object | Optional requestsPerMinute and retryAfterMs hints. |
Ink & Echo accepts capabilities as either an object or an array. Object keys with truthy values become capability names.
{
"capabilities": {
"search": true,
"discover": true,
"enrichment": true
}
}Search configuration tells Ink & Echo how to call your API and where the result records live in the JSON response.
"search": {
"request": {
"method": "GET",
"url": "https://api.example.com/catalog?q={QUERY}",
"headers": {
"Accept": "application/json"
},
"timeout": 15000
},
"response": {
"type": "json",
"resultsPath": "data.books",
"mapping": {
"title": "name",
"author": "credits.author",
"narrator": "credits.narrator",
"cover": "images.cover",
"description": "summary"
}
}
}Discovery sections appear as curated rows. Each section needs its own request and response parser.
"discover": {
"sections": [
{
"id": "new-releases",
"title": "New Releases",
"icon": "sparkles",
"request": {
"method": "GET",
"url": "https://api.example.com/books/new"
},
"response": {
"type": "json",
"resultsPath": "items",
"mapping": {
"title": "title",
"author": "author",
"cover": "coverUrl"
}
}
}
]
}response on every section.Mapping values use dot and bracket paths. For example, authors[0].namereads the first author name from an array.
| Field | Type | Description |
|---|---|---|
| title* | path | Book title. |
| author* | path | Primary author. Arrays can use index paths like authors[0].name. |
| subtitle | path | Subtitle. |
| narrator | path | Narrator for audiobook metadata. |
| series, seriesIndex | path | Series name and position. |
| description | path | Synopsis or catalog description. |
| cover | path | Cover image URL or a value transformed by response.templates.cover. |
| language | path | Language label or code. |
| publisher | path | Publisher name. |
| publishedYear, releaseDate | path | Year or date fields. |
| isbn, asin | path | External identifiers. |
| runtimeMinutes | path | Runtime in minutes. |
| rating | path | Rating value. |
| genres, categories, tags | path | Genre/category/tag values. |
"response": {
"type": "json",
"resultsPath": "books",
"mapping": {
"title": "volume.title",
"author": "volume.authors[0].name",
"publishedYear": "volume.year"
},
"templates": {
"cover": "https://img.example.com/covers/{value}.jpg"
}
}Static entries are useful for private catalogs, test data, or small curated metadata sets.
{
"kind": "metadata",
"id": "private-notes",
"name": "Private Notes",
"trustLabel": "Local",
"lawfulNote": "Personal metadata maintained by the user.",
"entries": [
{
"title": "Example Book",
"author": "Example Author",
"narrator": "Example Narrator",
"series": "Example Series",
"seriesIndex": "1",
"description": "A private metadata record.",
"cover": "https://example.com/cover.jpg",
"genres": ["Fiction"]
}
]
}{
"kind": "metadata",
"id": "example-audio-catalog",
"name": "Example Audio Catalog",
"version": "1.0.0",
"description": "Searches a fictional audiobook catalog API.",
"trustLabel": "User Added",
"lawfulNote": "Use only with APIs and metadata you are allowed to access.",
"capabilities": {
"search": true,
"discover": true
},
"search": {
"request": {
"method": "GET",
"url": "https://api.example.com/audiobooks/search?q={QUERY}",
"headers": {
"Accept": "application/json"
},
"timeout": 15000
},
"response": {
"type": "json",
"resultsPath": "data.items",
"mapping": {
"title": "title",
"author": "author.name",
"narrator": "narrator.name",
"cover": "images.large",
"description": "description",
"runtimeMinutes": "durationMinutes",
"series": "series.name",
"seriesIndex": "series.position",
"genres": "genres"
}
}
},
"discover": {
"sections": [
{
"id": "popular",
"title": "Popular",
"icon": "sparkles",
"request": {
"method": "GET",
"url": "https://api.example.com/audiobooks/popular"
},
"response": {
"type": "json",
"resultsPath": "data.items",
"mapping": {
"title": "title",
"author": "author.name",
"cover": "images.large"
}
}
}
]
},
"rateLimit": {
"requestsPerMinute": 20,
"retryAfterMs": 3000
}
}Sources
Source providers return records that can become downloadable or playable library items. A source must include remote search, static results, or discovery sections.
| Field | Type | Description |
|---|---|---|
| kind* | "search" | Declares this file as a source provider. |
| id | string | Stable identifier. Recommended for updates and persistence. |
| name or label* | string | Display name shown under Sources. |
| description | string | Short provider summary. |
| trustLabel | string | Human-readable trust/source label. |
| lawfulNote | string | Explains what the source is for and what the user is responsible for. |
| search | object | Remote request and response parser. |
| results | array | Static records, useful for samples or private fixed catalogs. |
| discover | object | Optional sections using the same request/response shape. |
| rateLimit | object | Optional throttling hints. |
audioUrl to a direct audio file or supported package URL.ebookUrl to epub, pdf, txt, or md content.magnet, magnetUrl, orinfoHash. Ink & Echo routes these through Real-Debrid or TorBox.Requests support GET and POST. Body templates can be strings, objects, or arrays.
"search": {
"request": {
"method": "POST",
"url": "https://api.example.com/search",
"headers": {
"Accept": "application/json",
"Content-Type": "application/json"
},
"body": {
"query": "{TITLE} {AUTHOR}",
"limit": 25
},
"timeout": 30000
},
"response": {
"type": "json",
"resultsPath": "hits",
"mapping": {
"title": "name",
"author": "author",
"audioUrl": "downloadUrl"
}
}
}resultsPath points to an array or object in the API response. If it resolves to an array, each object becomes a candidate result.
"response": {
"type": "json",
"resultsPath": "data.results",
"mapping": {
"title": "title",
"author": "creator",
"archiveUrl": "package.url",
"fileType": "package.format",
"sizeBytes": "package.bytes"
}
}At least one usable media field is needed for a source result to become actionable:audioUrl, ebookUrl,archiveUrl, magnet, orinfoHash.
| Field | Type | Description |
|---|---|---|
| title* | path | Result title shown in search. |
| author | path | Author used for filtering and metadata matching. |
| audioUrl | path | Direct audio file or package URL. |
| ebookUrl | path | Direct ebook URL. Supported file names include epub, pdf, txt, and md. |
| archiveUrl | path | Package URL such as a zip file that the import pipeline can scan. |
| magnet or magnetUrl | path | Magnet link. Ink & Echo routes it through the selected debrid service. |
| infoHash | path | Torrent info hash, also routed through debrid. |
| format | path | audiobook, ebook, or hybrid when the source provides it. |
| access | path | Optional direct or debrid hint. Ink & Echo also infers this from URLs. |
| fileType | path | Container or format label such as M4B, MP3, EPUB, PDF, ZIP. |
| sizeBytes | path | Byte size. Aliases like size, bytes, fileSize, and contentLength are supported. |
| seeders, leechers | path | Availability hints for debrid-backed results. |
{
"kind": "search",
"id": "public-audio-example",
"name": "Public Audio Example",
"description": "Fictional public-domain audio API.",
"trustLabel": "Public Catalog",
"lawfulNote": "Only returns public-domain media from this fictional API.",
"capabilities": {
"search": true,
"acquire": true
},
"search": {
"request": {
"method": "GET",
"url": "https://api.example.com/public-audio?q={QUERY}",
"headers": {
"Accept": "application/json"
},
"timeout": 15000
},
"response": {
"type": "json",
"resultsPath": "items",
"mapping": {
"title": "title",
"author": "author",
"audioUrl": "audio.downloadUrl",
"cover": "coverUrl",
"fileType": "audio.format",
"runtimeMinutes": "durationMinutes"
}
}
}
}{
"kind": "search",
"id": "private-index-example",
"name": "Private Index Example",
"description": "Fictional private index that returns magnet links.",
"trustLabel": "Private",
"lawfulNote": "User is responsible for the index and content they access.",
"capabilities": {
"search": true,
"debrid": true
},
"search": {
"request": {
"method": "POST",
"url": "https://api.example.com/index/search",
"headers": {
"Accept": "application/json",
"Content-Type": "application/json"
},
"body": {
"query": "{TITLE} {AUTHOR}"
},
"timeout": 30000
},
"response": {
"type": "json",
"resultsPath": "results",
"mapping": {
"title": "name",
"author": "author",
"magnet": "magnet",
"infoHash": "hash",
"sizeBytes": "size",
"seeders": "seeders",
"leechers": "leechers"
}
}
},
"rateLimit": {
"requestsPerMinute": 30,
"retryAfterMs": 2000
}
}Runtime
Ink & Echo currently substitutes three placeholders in request URLs, headers, and bodies.
| Field | Type | Description |
|---|---|---|
| {QUERY}* | string | The raw search query. |
| {TITLE} | string | The selected title, falling back to query. |
| {AUTHOR} | string | The selected author, or an empty string. |
Services
Fixes
Help