শঙ্খচিল 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/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
countryoptionalBD, IN, CA or US. Defaults to where the request comes from.
as_ofoptional, YYYY-MM-DDthe verdicts as of that date. A malformed date is a 400, never silently today.
authoroptionalrestrict 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
countryoptionalBD, IN, CA or US. Defaults to where the request comes from.
as_ofoptional, YYYY-MM-DDwhat 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.
availableoptional, true or falsereturn only available, or only withheld, works
authoroptionalrestrict 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
    }
  ]
}
countsdescribes 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/worksguaranteed, 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
themerequireda phrase, not a question — a mood, an image, a subject
describealternative to themewhat is IN a photograph, for captioning
limitoptional, 1–20, default 5how many lines
kindoptional, verse or proserestrict 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
    }
  ]
}
provenancealways 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 guaranteeevery 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
qrequiredthe question
limitoptional, 1–10, default 5how many passages to retrieve
composeroptionalextractive 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": ["…"]
}
composergenerative 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_provenancewho wrote the sentences BETWEEN the quotations — model-generated or corpus-assembled. Stated on every response, never inferred from composer by the caller.
country_basisan 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_checkedhow many citations were verified against the corpus. A positive count, not a boolean, so "checked and clean" is distinguishable from "never checked".
modelpresent only when a model wrote the prose. An extractive answer has no model, so the key is absent rather than null.
composer_notespresent 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
qrequiredthe question
limitoptional, 1–10, default 5how many passages to retrieve
composeroptionalextractive to skip the generative pass
request_idoptional, body or x-request-ididempotency 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 / balancewhat this question cost and what is left, from the ledger, never estimated. A cached answer says charged: 0.
401 / 402 / 403 / 429unknown 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.
refundedon 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

400a required parameter is missing or malformed
401no key, or an unknown key, on a metered endpoint
429over your key's limit. Retry-After is set.
451the work is not public domain in the caller's country
500our fault. verbatim_check_failed means a quotation did not match the source and the answer was refused rather than served.
503metering 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.