Contents

  1. Quickstart
  2. What you must show
  3. Authentication
  4. Limits
  5. Service level
  6. Errors
  7. Endpoints
  8. GET/​document-types
  9. POST/​drafts
  10. GET/​drafts/​:job_id
  11. GET/​drafts/​:job_id/​files/​docx
  12. GET/​drafts/​:job_id/​files/​docx-preeti
  13. DEL/​drafts/​:job_id
  14. GET/​balance
  15. GET/​usage
  16. POST/​drafts/​:job_id/​revisions
  17. GET/​drafts/​:job_id/​revisions/​:revision_id
  18. POST/​drafts/​:job_id/​revisions/​:revision_id/​apply
  19. POST/​drafts/​:job_id/​revisions/​:revision_id/​discard
  20. Job states
  21. Credits
  22. Revising and hand-editing
  23. Retention and deletion
  24. Where data is processed

For partners · v1

Likhat Partner API

Create, revise and download Nepali legal drafts from your own product.

Contents
  1. Quickstart
  2. What you must show
  3. Authentication
  4. Limits
  5. Service level
  6. Errors
  7. Endpoints
  8. GET/​document-types
  9. POST/​drafts
  10. GET/​drafts/​:job_id
  11. GET/​drafts/​:job_id/​files/​docx
  12. GET/​drafts/​:job_id/​files/​docx-preeti
  13. DEL/​drafts/​:job_id
  14. GET/​balance
  15. GET/​usage
  16. POST/​drafts/​:job_id/​revisions
  17. GET/​drafts/​:job_id/​revisions/​:revision_id
  18. POST/​drafts/​:job_id/​revisions/​:revision_id/​apply
  19. POST/​drafts/​:job_id/​revisions/​:revision_id/​discard
  20. Job states
  21. Credits
  22. Revising and hand-editing
  23. Retention and deletion
  24. Where data is processed

Base URL: https://api.likhatai.com. JSON over HTTPS. All paths start with /v1/partner.

A draft is made asynchronously: you create a job, poll it, then fetch the text and the Word files.

Every draft is AI-generated and not lawyer-verified. It must not be filed without a licensed advocate's review (see What you must show).

Quickstart

Use your lk_test_… sandbox key: it runs every check and error path but no model, charges nothing, and returns a fixed sample draft at once.

1. Pick a document type.

curl https://api.likhatai.com/v1/partner/document-types \
  -H "x-partner-key: $LIKHAT_KEY"

2. Create a draft. The Idempotency-Key is required; retrying with the same key and body never starts a second draft.

curl -X POST https://api.likhatai.com/v1/partner/drafts \
  -H "x-partner-key: $LIKHAT_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "document_type": "bail-application", "brief": "Client was arrested on 2082-05-14 on a cheque dishonour complaint…", "language": "ne" }'

3. Poll every 10 seconds until it ends. Respect Retry-After; do not poll faster.

curl https://api.likhatai.com/v1/partner/drafts/$JOB_ID \
  -H "x-partner-key: $LIKHAT_KEY"

4. Download the Word file. Unicode Devanagari, or docx-preeti for the Preeti font.

curl -o draft.docx https://api.likhatai.com/v1/partner/drafts/$JOB_ID/files/docx \
  -H "x-partner-key: $LIKHAT_KEY"

5. Revise it. Echo back the version from the last response as base_version.

curl -X POST https://api.likhatai.com/v1/partner/drafts/$JOB_ID/revisions \
  -H "x-partner-key: $LIKHAT_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Shorten paragraph 2.", "base_version": "'"$VERSION"'" }'

A polling loop that respects Retry-After:

const BASE = "https://api.likhatai.com/v1/partner";
const headers = { "x-partner-key": process.env.LIKHAT_KEY };
const OPEN = new Set(["pending", "queued", "running"]);

async function waitForDraft(jobId) {
  for (;;) {
    const res = await fetch(`${BASE}/drafts/${jobId}`, { headers });
    const body = await res.json();
    const retry = [409, 429, 503].includes(res.status); // each carries Retry-After
    if (!res.ok && !retry) throw new Error(`${body.error.code} (${body.error.request_id})`);
    if (res.ok && !OPEN.has(body.status)) return body; // succeeded | refused | failed | cancelled
    const wait = Number(res.headers.get("retry-after") ?? 10);
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
}

What you must show

Contract requirements, not suggestions:

  1. Always show draft.notice to the lawyer reading the draft, as a visible banner. Never remove it, shorten it or move it out of sight. The same notice is also the first block of draft.text and is inside the Word files.
  2. Always show draft.review_notes to the lawyer. It lists blanks, unverified items and points that need a human check.
  3. Show draft.sources with the draft where your UI has room for footnotes.
  4. A Likhat draft is never "verified" because it came from Likhat. Do not label it so.

Authentication

Send your key in the x-partner-key header. Never in a query string.

x-partner-key: lk_live_1a2b3c4d_<43 characters>
  • lk_test_… keys are sandbox keys: same validation, idempotency and errors, and the same request rate limit, but no model runs and nothing is charged. The daily draft cap does not apply to them, and the drafting checks (for example Preeti-text detection or thin-brief refusal) are not run. A create returns succeeded at once with one fixed sample draft; the Word files are fixed samples. A revision returns one fixed applied result: the sample draft is unchanged, so its version stays the same (base_version is still checked against it). Use them to build and test your integration.
  • lk_live_… keys are issued only once the production Anthropic API-key switch is complete. Until then you have a test key only.
  • Keys are issued by Likhat out of band and shown once. You can hold two live keys at a time so you can rotate without downtime. A revoked key stops working immediately.
  • A missing, malformed or revoked key is 401 unauthorized.

Limits

LimitValue
Requests120 per minute per key (429 rate_limited, with Retry-After)
Creating drafts5 per minute per partner account, also enforced on our side by the drafting route itself (answered as 503 capacity with Retry-After, never 429)
New drafts50 per day, counted from 00:00 Kathmandu time (429 rate_limited, Retry-After = seconds to the next day)
Brief size60,000 characters
Revision message20,000 bytes of UTF-8 (a Devanagari character is 3 bytes, so about 6,600 characters)
Revisions running at once1 per draft (409 job_in_flight, Retry-After: 10), and 1 across all of Likhat's partners (429 rate_limited, Retry-After: 120)
Your drafts running at oncecurrently up to 4 per partner, the rest wait in the queue (may change)

Service level

Best effort. No guarantee. A draft typically completes in 20–40 minutes. Partner drafts share one queue with Likhat's own lawyers, so a busy period delays yours. While a job is queued, the response carries queue.position and queue.eta_seconds.

Poll GET /v1/partner/drafts/:job_id every 10 seconds. Poll responses carry Retry-After: 10. Do not poll faster. A job that waits more than 26 hours in the queue is closed as failed with queue_timeout. It is charged only for spend already incurred (zero if the job never started).

Errors

Every error has the same shape:

{ "error": { "code": "insufficient_credits", "request_id": "req-8f2c" } }

detail is sometimes present. Quote request_id when you contact us.

HTTPcodeMeaningWhat to do
400invalid_requestBad body, unknown document_type, missing Idempotency-Key, or the brief is not Unicode Nepali/English text (detail: "preeti_text")Fix the request
401unauthorizedKey missing, wrong or revokedCheck the key
402insufficient_creditsBalance too low to start a draft, or under 50 credits when you start a revisionAsk us to top up
404not_foundNo such job for your account (also another partner's id, or a removed job)—
409job_in_flightThe draft is not succeeded yet, another operation on it is in progress, a create or revise with this Idempotency-Key is still being accepted, or a proposal can no longer be decided (stale or already decided)Wait Retry-After seconds, then poll or retry the same request. A proposal answer has no Retry-After: GET the draft for its current text
409idempotency_conflictSame Idempotency-Key, different bodyUse a new key
409version_conflictbase_version is not the draft's current version: another revision changed it since you pulled itGET the draft, replace your copy, revise again. See Revising and hand-editing
429rate_limitedOver a limit above, or a revision of another draft (or another partner) is running (Retry-After: 120)Wait Retry-After seconds
503capacityQueue full, disk low, provider quota, or metering down. Nothing was started or chargedRetry after Retry-After seconds
500internalOur faultRetry once; then contact us with request_id

Endpoints

GET/v1/partner/document-types

The valid document_type values.

{ "document_types": [ { "id": "bail-application", "ne": "धरौटी निवेदन", "en": "Bail application" } ] }

ne / en can be null.

POST/v1/partner/drafts

Start a draft. The Idempotency-Key header is required (1–200 characters, ASCII, no spaces). Retrying with the same key and the same body returns the same job and never starts a second draft; the same key with a different body is 409 idempotency_conflict. Keys are remembered for at least 24 hours. If a create is refused (credits, validation), the key can be used again. Repeating a create while the first one is still being accepted (a few seconds) answers 409 job_in_flight with Retry-After: 5: wait, then repeat the same request to get the job.

Body (unknown fields are rejected):

Field
document_typerequiredan id from /document-types
briefrequiredthe facts and instructions, Nepali or English, 1–60,000 chars
languageoptionalne (default) or en
partiesoptionalup to 20 × { "name", "role" }
forumoptionalcourt or office
advocateoptional{ "name", "advocate_no", "firm", "address" } (only name required). This is the only source of the advocate's identity on the draft. If absent, the draft has blanks (__________).
firm_ref, user_refoptionalyour own opaque strings, up to 200 chars. Stored for audit; not checked and not returned.
POST /v1/partner/drafts
x-partner-key: lk_live_…
Idempotency-Key: 6f1c1d52-0c3e-4c8e-9a58-2f1b1f0f7a11
Content-Type: application/json

{
  "document_type": "bail-application",
  "brief": "Client was arrested on 2082-05-14 on a cheque dishonour complaint…",
  "language": "ne",
  "parties": [{ "name": "Ram Bahadur Thapa", "role": "applicant" }],
  "advocate": { "name": "Sita Sharma", "advocate_no": "12345" }
}
202 Accepted
{
  "job_id": "pj_3f9a1c0b5d7e2a4c6b8d0e1f",
  "status": "queued",
  "created_at": "2026-11-02T09:15:00.000Z",
  "queue": { "position": 3, "eta_seconds": { "from": 1200, "to": 2400 } }
}

queue is present when known. If the job is still being handed to the drafter, status is pending.

GET/v1/partner/drafts/:job_id

Status, and the result once succeeded.

Queued:

{
  "job_id": "pj_3f9a1c0b5d7e2a4c6b8d0e1f",
  "status": "queued",
  "created_at": "2026-11-02T09:15:00.000Z",
  "queue": { "position": 1, "eta_seconds": { "from": 600, "to": 1500 } }
}

Succeeded:

{
  "job_id": "pj_3f9a1c0b5d7e2a4c6b8d0e1f",
  "status": "succeeded",
  "created_at": "2026-11-02T09:15:00.000Z",
  "finished_at": "2026-11-02T09:41:12.000Z",
  "credits_charged": 540,
  "version": "9c1185a5c5e9fc54612808977ee8f548b2258d31b8d8c9d7c2f4a1e6b0a3d4f1",
  "draft": {
    "text": "## ⚠️ AI-generated draft — not lawyer-verified\n\nReview every citation, fact, and clause before relying on it. Do not file without a licensed advocate's review.\n\n---\n\n…",
    "notice": "## ⚠️ AI-generated draft — not lawyer-verified\n\nReview every citation, fact, and clause before relying on it. Do not file without a licensed advocate's review.\n\n---\n",
    "review_notes": "- Blank: date of arrest\n- Unverified: …",
    "sources": "[1] …",
    "checklist": null,
    "document_type": "bail-application",
    "language": "ne"
  }
}

notice is a Markdown block (a heading, a paragraph and a rule); render it as Markdown or strip the markup, but show all of its words. review_notes, sources, checklist are null when the draft has none. document_type and language are null in the rare case the service cannot read them back; they are never guessed.

credits_charged on a queued or running poll is provisional; it is final only in a terminal state (succeeded, refused, failed, cancelled).

Refused or failed:

{ "job_id": "pj_…", "status": "refused", "created_at": "…", "finished_at": "…", "error_code": "refused_thin_brief", "credits_charged": 120 }

GET/v1/partner/drafts/:job_id/files/docx

GET/v1/partner/drafts/:job_id/files/docx-preeti

The draft as a Word file: Unicode Devanagari, or Preeti font. Binary body, Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document, with a Content-Disposition filename. Only for a succeeded job: a job that is still open, or ended without a draft, returns 409 job_in_flight; an unknown, deleted or purged job returns 404 not_found. The Word files contain the same notice as draft.text.

DEL/v1/partner/drafts/:job_id

Delete a job and its data. A queued or running job is cancelled first (status: cancelled); spend already incurred is still charged. The response is 204 with no body, and repeating the call also returns 204. Afterwards the job reads as 404 not_found. See Retention.

GET/v1/partner/balance

{ "balance": 41200 }

Credits only. No currency, tokens or rates. With a test key the balance is a fixed 100000.

GET/v1/partner/usage?from=2026-11-01&to=2026-11-30

Credits charged per job in the period, newest first, at most 1,000 rows. Deleted jobs are included. from, to are ISO 8601 dates or timestamps, read as UTC (the daily draft cap counts Kathmandu days, so a day here is not the same window). A bare date to covers that whole day; a timestamp to is exclusive. A test key sees only sandbox rows.

{ "jobs": [ { "job_id": "pj_…", "kind": "draft", "created_at": "2026-11-02T09:15:00.000Z", "credits_charged": 540 } ] }

POST/v1/partner/drafts/:job_id/revisions

Revise a succeeded draft with an instruction ("shorten paragraph 2", "add the date of arrest"). The Idempotency-Key header is required, with the same rules as a create: the same key and body returns the same revision and never starts a second one.

Body (unknown fields are rejected):

Field
messagerequiredthe instruction, 1–20,000 bytes of UTF-8, not blank. Anything else is 400 invalid_request.
base_versionrequiredthe version Likhat last returned for this draft, echoed back unchanged. Missing is 400 invalid_request; not the current version is 409 version_conflict. See Revising and hand-editing.
scopeoptional{ "para": 2, "text": "The applicant was…" }: aim the instruction at one paragraph. para is a whole paragraph number, 1 or more (400 invalid_request otherwise), text the paragraph's opening words (up to 80 characters; longer is cut). Likhat finds the paragraph by text first and by number second, so send both. A scope that matches no paragraph gives an advisory result and changes nothing.
POST /v1/partner/drafts/pj_3f9a1c0b5d7e2a4c6b8d0e1f/revisions
x-partner-key: lk_live_…
Idempotency-Key: 0b8d1f0e-6a43-4a0b-9d4c-3e5f6a7b8c9d
Content-Type: application/json

{ "message": "Shorten paragraph 2.", "base_version": "9c1185a5c5e9fc54612808977ee8f548b2258d31b8d8c9d7c2f4a1e6b0a3d4f1" }
202 Accepted
{ "revision_id": "pr_4a7c9e1b3d5f7a9c1e3b5d7f", "job_id": "pj_3f9a1c0b5d7e2a4c6b8d0e1f", "status": "running" }

Checks run in this order, and a refused request starts nothing and charges nothing (its key can be used again):

  1. 400 invalid_request: bad body or missing Idempotency-Key. 404 not_found: no such draft for your account.
  2. 409 job_in_flight: the draft is not succeeded (no Retry-After; GET the draft for its state), or one of its own revisions is still running (Retry-After: 10).
  3. 402 insufficient_credits: balance under 50 credits.
  4. 429 rate_limited with Retry-After: 120: a revision of another draft is running. Only one runs at a time across all partners, so this can happen even if you have none running. Wait, then retry.
  5. 409 version_conflict: base_version is not the draft's current version.
  6. 503 capacity (with Retry-After): Likhat's chat capacity is busy or the metering is down. 409 job_in_flight: a lawyer on the same draft has a chat turn running.

If the draft's previous revision is a proposal nobody decided, this request discards it first and the response says "discarded_previous": true. That discard stands even if the new request is then refused at step 6.

GET/v1/partner/drafts/:job_id/revisions/:revision_id

Poll every 10 seconds (responses carry Retry-After: 10 while it is open). An unknown revision, one that belongs to another draft or account, or a removed draft is 404 not_found. A revision usually takes a few minutes. One still open after 17 minutes is closed as failed with timeout.

While it runs: { "revision_id": "pr_…", "job_id": "pj_…", "status": "running" }. Once status is succeeded, type says what happened. A revision whose status is cancelled is a proposal that was discarded, by you or by a newer revision; the draft is unchanged and there is no type.

typeMeaningFields
appliedThe edit passed our checks and is now the draft.change_note, version (the new one), draft (the whole current draft, same shape as in GET /drafts/:job_id)
proposalThe edit is held because our checks flagged it. The draft is unchanged until you apply it.change_note, hold_reasons
advisoryAn answer, not an edit. The draft is unchanged.reply, unverified: true
rejectedThe edit failed our checks. The draft is unchanged.change_note when there is one
failedstatus is failed.error_code: timeout or upstream. Nothing was applied; send the instruction again.
{
  "revision_id": "pr_4a7c9e1b3d5f7a9c1e3b5d7f",
  "job_id": "pj_3f9a1c0b5d7e2a4c6b8d0e1f",
  "status": "succeeded",
  "type": "applied",
  "change_note": "Shortened paragraph 2.",
  "version": "5d0b0f2f7a1c4e6b8d9a0c1e3f5a7b9d1c3e5f7a9b1d3f5a7c9e1b3d5f7a9c1e",
  "draft": { "text": "…", "notice": "…", "review_notes": "…", "sources": null, "checklist": null, "document_type": "bail-application", "language": "ne" }
}

Read hold_reasons before you apply a proposal. It lists, one string per check that held the edit, the same plain-language reason Likhat's own lawyers see (for example: The edit adds "दफा ९५", which could not be checked against the statute. Verify it before applying.). Show it to the lawyer next to the change: applying a held edit means they have read why it was held. The field is left out (like change_note) when there is nothing to list, and it never appears on other types. Like change_note, each string is plain text with Likhat's internal references (citation tokens, storage paths) removed.

An advisory reply is not checked. It bypasses the citation checks that edits go through, so any law or case it names may be wrong. Show it to the lawyer as unverified. change_note and reply are plain text with Likhat's internal references removed.

An applied result carries the draft as it is when you fetch it, so a later revision shows through an earlier one's draft. Always replace your copy with the newest version and draft you have.

POST/v1/partner/drafts/:job_id/revisions/:revision_id/apply

POST/v1/partner/drafts/:job_id/revisions/:revision_id/discard

Settle a proposal. Send {}, or no body without a JSON Content-Type. apply makes the held edit the draft and returns 200 with the applied shape above (draft and the new version). discard drops it and returns 200 with status: "cancelled"; the draft is unchanged. Neither costs credits.

Only an undecided proposal can be settled; anything else answers 409 job_in_flight. A revision that is still running: poll it until it is a proposal. A proposal that can no longer be decided (already applied or discarded, stale because the draft changed after it was made, or decided by a lawyer at Likhat), and an advisory / rejected / failed revision: the 409 has no Retry-After, because waiting does not help. GET the draft for its current text. If apply answers 5xx, GET the revision: the proposal may already have been applied.

Job states

pending → queued → running → succeeded | refused | failed | cancelled
StateMeaning
pendingAccepted; being handed to the drafter (seconds)
queuedWaiting in line. See queue
runningBeing drafted
succeededDone. draft and version are present
refusedWe would not draft this. error_code: refused_thin_brief (brief too thin) or refused_unsupported_type. Add detail to the brief and create a new job
failedNot completed. error_code: timeout, queue_timeout, worker_lost, upstream, internal. Create a new job
cancelledYou deleted it before it finished

Terminal states never change. Jobs that fail because the model connection dropped or the drafting worker was lost are not charged. Refusals, timeouts, cancellations and other failures are charged for what they used; credits_charged says how much.

Credits

Credits are checked when you create a draft. If your balance cannot cover the worst case of all your drafts in flight, you get 402 insufficient_credits and nothing starts. Credits are topped up by Likhat after invoice. Per-job charges are in credits_charged and /usage.

A revision needs a balance of at least 50 credits to start. It is charged to your balance for what it used, whatever the outcome (applied, proposal, advisory, rejected or failed); apply and discard are free. /usage lists revisions, but its credits_charged for them is 0 for now: read /balance for the true amount.

Revising and hand-editing

Every draft and revision response carries a version: a hash that Likhat computes from the draft text that response returned. To revise, send it back as base_version, echoed unchanged: the version Likhat last gave you for this draft, never a hash of text you computed yourself. If it is not the draft's current version (another revision landed in between), you get 409 version_conflict. There is no force option.

Likhat revises its own stored text only. Hand edits made in your editor are not sent to Likhat, and a revision does not see them. So: revise first, hand-edit last. After a revision, replace your copy with the returned draft. Warn your user before a revision overwrites local edits. To tell whether they edited, compare your local text with the text of the version you pulled; Likhat cannot see that, and version_conflict only detects a competing revision.

Retention and deletion

  • Drafts you do not delete are removed 30 days after last activity.
  • After DELETE, the draft is removed from our live storage within 24 hours.
  • Backups keep it longer. Nightly backups hold copies for up to 14 days after a deletion. Once our off-box backup bucket is live, up to 30 days.
  • Your brief is held in our database until the job finishes, then cleared. Logs carry ids, never brief or draft text.
  • Revision chat history is stored with the draft until removed.

Where data is processed

Requests are processed on our servers in Bangalore, India (DigitalOcean), and the drafting model is run by Anthropic in the United States. As the controller, you are responsible for obtaining your clients' consent to this processing.