শঙ্খচিল API
সম্পূর্ণ পাবলিক-ডোমেইন বাংলা সাহিত্যের উপর খোঁজ ও উদ্ধৃতিসহ উত্তর। A read-only HTTP API over the public-domain Bengali canon.
Before anything else: two things that will bite you
Availability depends on the caller's country. Copyright terms differ between Bangladesh, India, Canada and the United States, and this API computes the verdict per work per country at request time. The same request from two countries legitimately returns different results. A response you got from one place does not establish that a work is free anywhere else — see the rules.
Ignore fields you do not recognise. New fields are added
inside /v1 without a new version; that is the whole basis of the
versioning
policy, and a client validating against a closed schema will break on a
change we consider safe.
Authentication
/v1/search, /v1/works and /v1/catalog are open and need no key.
/v1/ask is metered: send your key as
Authorization: Bearer <key> or X-API-Key.
A successful metered response carries
X-RateLimit-Remaining. Over your limit you get
429 with Retry-After. There is no public sign-up
yet — the developer tier is not built (sk-24), so keys are issued by hand.
GET /v1/search
open
Phrase search across every work the caller's country permits. Free and unmetered — it costs no model call, and charging for it would push a caller toward the metered endpoint for something the corpus answers for nothing.
| parameter | ||
|---|---|---|
q | required | the phrase to look for |
limit | optional, 1–20, default 8 | how many passages to return |
Response
{
"q": "নদী",
"country": "IN",
"count": 8,
"hits": [
{
"passage_id": "0c0b3e9856a919ee",
"work_id": "sukumar-ray-ca3f2ef613",
"section_index": 68,
"offsets": [177596, 178129],
"kind": "verse",
"text": "…",
"score": 0.4131
}
]
}
GET /v1/works
open
Metadata and rights, never text. This is how a consumer asks what may be published in a market instead of hardcoding a list of ids — a hardcoded list is a copy of the rights rules that stops being true on the next 1 January. corpus_version names the corpus release the API is serving, so a consumer can pin against it; if the database ever holds a partial load, corpus_version is null and corpus_versions_mixed lists what was found.
| parameter | ||
|---|---|---|
country | optional | BD, IN, CA or US. Defaults to where the request comes from. |
as_of | optional, YYYY-MM-DD | the verdicts as of that date. A malformed date is a 400, never silently today. |
author | optional | restrict to one author, by either name form |
Response
{
"country": "IN",
"as_of": "2026-09-01",
"corpus_version": "corpus-3a0a6f04e85a",
"count": 246,
"works": [
{
"id": "rabindranath-tagore-f898bbcb80",
"title": "সোনার তরী (১৮৯৩)",
"author": "Rabindranath Tagore",
"author_bn": "রবীন্দ্রনাথ ঠাকুর",
"death_year": 1941,
"first_pub_year": 1893,
"chars": 111965,
"available": true,
"reason_bn": null,
"available_from": null
}
]
}
GET /v1/catalog
open
The per-country manifest: the same rights verdicts as /v1/works, plus counts, a section count, and the ability to ask for only what is available. Use this when the question is "what may I publish in this market" and you want the answer already filtered.
| parameter | ||
|---|---|---|
country | optional | BD, IN, CA or US. Defaults to where the request comes from. |
as_of | optional, YYYY-MM-DD | what the catalogue looked like, or will look like, on that date — the question every 1 January makes interesting. A malformed date is a 400, never silently today. |
available | optional, true or false | return only available, or only withheld, works |
author | optional | restrict to one author, by either name form |
Response
{
"country": "IN",
"as_of": "2026-09-01",
"counts": { "total": 246, "available": 246 },
"works": [
{
"id": "rabindranath-tagore-f898bbcb80",
"title": "সোনার তরী (১৮৯৩)",
"author": "Rabindranath Tagore",
"author_bn": "রবীন্দ্রনাথ ঠাকুর",
"death_year": 1941,
"first_pub_year": 1893,
"chars": 111965,
"sections": 42,
"available": true,
"reason_bn": null,
"available_from": null
}
]
}
counts | describes the entries RETURNED, so it always adds up against works beside it — a filtered list never reports the whole corpus's total. |
agreement with /v1/works | guaranteed, not coincidental. Both endpoints get their verdict from one computation, and a test asserts they agree work-by-work for every country. Two endpoints answering one rights question differently is the failure this API has already had twice. |
GET, POST /v1/quote-for
key required
A theme in, apt lines out. For a caption, a card, a title sequence — anywhere you want a line that FITS rather than an answer to a question. It returns quotations and nothing else: no composed prose, and no model chose or rewrote them.
| parameter | ||
|---|---|---|
theme | required | a phrase, not a question — a mood, an image, a subject |
describe | alternative to theme | what is IN a photograph, for captioning |
limit | optional, 1–20, default 5 | how many lines |
kind | optional, verse or prose | restrict the form |
Response
{
"theme": "বর্ষার নদী",
"country": "IN",
"count": 2,
"provenance": "corpus-retrieved",
"citations": [
{
"work_id": "rabindranath-tagore-f898bbcb80",
"work_title": "সোনার তরী (১৮৯৩)",
"author": "Rabindranath Tagore",
"author_bn": "রবীন্দ্রনাথ ঠাকুর",
"section_index": 12,
"offsets": [177596, 178129],
"kind": "verse",
"text": "…",
"verbatim": true
}
]
}
provenance | always corpus-retrieved. Nothing composed, selected or rewrote these lines — retrieval ranked them and the corpus supplied them, which is why there is no answer field for a model to fill later. |
| the same guarantee | every line is cut from canonical text by offset and verified before the response leaves, exactly as on /v1/ask. A quotation service that could not prove its quotations would be a machine for attributing invented Bengali to real poets. |
GET, POST /v1/ask
key required
A composed answer in which every quotation is verbatim from the corpus, checked against the canonical text at request time. Metered per key.
| parameter | ||
|---|---|---|
q | required | the question |
limit | optional, 1–10, default 5 | how many passages to retrieve |
composer | optional | extractive to skip the generative pass |
Response
{
"question": "নদী কী?",
"country": "CA",
"country_basis": {
"host": "shangkhachil.com",
"geo": "CA",
"stated": null,
"countries": ["CA"],
"source": "edge"
},
"answer": "…",
"answer_provenance": "model-generated",
"citations": [
{
"work_id": "sukumar-ray-ca3f2ef613",
"work_title": "…",
"author_bn": "সুকুমার রায়",
"section_index": 68,
"offsets": [177596, 178129],
"kind": "verse",
"text": "…",
"verbatim": true
}
],
"composer": "extractive",
"tokens_used": 1930,
"retrieval": { "lexical": 8, "dense": 8, "allow_size": 243, "…": null },
"verbatim_checked": 2,
"composer_notes": ["…"]
}
composer | generative or extractive. It degrades to extractive when the model is unavailable or when its answer fails the prose guard, and says so in composer_notes rather than silently. |
answer_provenance | who wrote the sentences BETWEEN the quotations — model-generated or corpus-assembled. Stated on every response, never inferred from composer by the caller. |
country_basis | an OBJECT, not a string: how the country was decided — geo, anything the caller stated, the whole countries set applied, and the source. A wrong availability answer is then debuggable rather than mysterious. |
verbatim_checked | how many citations were verified against the corpus. A positive count, not a boolean, so "checked and clean" is distinguishable from "never checked". |
model | present only when a model wrote the prose. An extractive answer has no model, so the key is absent rather than null. |
composer_notes | present only when the composer degraded — the model was unavailable, or its answer failed the prose guard. Their absence is the normal case. |
GET, POST /v1/dev/ask
key required
The same answer, same verbatim guarantee, for a personal developer key. Create and manage keys at /ডেভেলপার/ — a key is minted once, stored only as a hash, and spends its owner's own credits: one credit per uncached question, cache hits free. Send it as Authorization: Bearer skdev_… (or x-api-key).
| parameter | ||
|---|---|---|
q | required | the question |
limit | optional, 1–10, default 5 | how many passages to retrieve |
composer | optional | extractive to skip the generative pass |
request_id | optional, body or x-request-id | idempotency key — a retried request with the same id is charged once |
Response
{
"…": "the /v1/ask response shape, plus",
"charged": 1,
"balance": 29,
"remaining_this_hour": 11,
"cached": false
}
charged / balance | what this question cost and what is left, from the ledger, never estimated. A cached answer says charged: 0. |
401 / 402 / 403 / 429 | unknown or revoked key · out of credit · key lacks the ask scope · hourly cap, with Retry-After. Out-of-credit and rate-capped are different problems and get different codes. |
refunded | on a failed answer the debit is returned as a new ledger row and the error says whether that worked — never a silent promise. |
The citation object
The point of this API. offsets are
[start, end) character offsets into the
canonical text of work_id, and text is exactly that
slice — so a quotation is checkable rather than trusted.
verbatim is true only when that slice was verified
against the corpus while answering; if verification cannot run, the request
fails rather than returning an unchecked citation.
A citation is a flat record and grows by gaining keys, never by reshaping what is there.
Errors and limits
400 | a required parameter is missing or malformed |
401 | no key, or an unknown key, on a metered endpoint |
429 | over your key's limit. Retry-After is set. |
451 | the work is not public domain in the caller's country |
500 | our fault. verbatim_check_failed means a quotation did not match the source and the answer was refused rather than served. |
503 | metering is unavailable. Requests are refused rather than served free. |
Latency, measured rather than promised
Typical figures from this deployment, not a guarantee:
/v1/search and /v1/works answer in a few hundred
milliseconds. /v1/ask takes ten to twenty seconds
on a question nobody has asked before — the model composition is most of
that, with retrieval and the verbatim verification of every quotation
around it — and answers a repeated question from cache in well under a
second.
A note on /v1/catalog
It returned 404 between the move to the current implementation and 2026-09-01, when it was restored. If you tried it in that window and concluded it did not exist, it does now.
Playground
This runs against the live API from your browser, so what you see is what your own country is served.
/v1/ask
Not runnable here, and it would be dishonest to fake it. The endpoint spends a key's quota, and this page has no key that is not somebody's. So it is documented rather than demonstrated: the response block above lists the fields the code actually returns, and the request is below. Run it with your own key and you will see the real thing.
curl -H "Authorization: Bearer $KEY" \
"https://shangkhachil.com/v1/ask?q=%E0%A6%A8%E0%A6%A6%E0%A7%80&limit=3"
The q above is নদী,
percent-encoded. The corpus is Bengali; send UTF-8 and encode it.