Source: https://legiscore.in/docs/search-lifecycle

# Search lifecycle

A search is asynchronous: submit it, it runs against the official source, read the result when it finishes.

```text
POST /api/v1/search              ->  202  { id, status: "queued" }
GET  /api/v1/search/{id}         ->  queued -> running -> succeeded | failed | cancelled
GET  /api/v1/search/{id}/documents/{filename}  ->  302 to a short-lived signed URL
```

## Submit

`POST /api/v1/search` takes one search, or up to 25 in `searches[]`. The charge is taken at submit. A request that fails validation returns 422 and charges nothing.

```json
{
  "state": "telangana",
  "search_type": "ec",
  "params": {
    "...": "see the search page"
  },
  "label": "Case 8841 EC",
  "callback_url": "https://your-app.example.com/hooks/legiscore"
}
```

In a batch each item is accepted or refused on its own. Refused items return in `rejected[]` as { index, code, message, retry_after? } and are not charged.

## Statuses

| status | Meaning | Terminal |
| --- | --- | --- |
| queued | Accepted and charged. Waiting for a slot. | No |
| running | The source is being queried. progress is an estimate 0-100. | No |
| awaiting_otp | The source asked for a one-time code. See OTP. | No |
| succeeded | Finished. found, records and documents are filled in. | Yes |
| failed | The source could not answer. Refunded. error says why. | Yes |
| cancelled | You cancelled it. Refunded when confirmed. | Yes |

A search not terminal after 3 hours is failed and refunded automatically. Poll every 5 to 10 seconds or use a webhook.

## Reading the result

| Field | Meaning |
| --- | --- |
| found | null until finished. false means the source answered and holds nothing under that query. |
| records[] | Rows the source returned. Shape depends on the search. |
| documents[] | { filename, content_type, bytes, withheld }. Download with the documents route. |
| note | A coverage caveat from the source, when there is one. |
| error | { kind, message } on a failed search. |
| refunded | Whether this search was refunded. |

Register searches: found:false is not a clear title. It means no per-property match was computed. Read the documents.

Failure kinds: portal_blocked (retry later), portal_timeout (retry), portal_changed (report to us), captcha_failed (retry), otp_timeout (retry), invalid_input (do not retry, fix parameters), internal (retry, then contact us).

## Documents

`GET /api/v1/search/{id}/documents/{filename}` answers 302 to a signed URL valid for one hour. A file that failed its integrity check returns 410 withheld.

## OTP

Some sources verify with a one-time code (searches marked OTP). LegiScore handles it, so the search may sit in awaiting_otp briefly and continue. If a search needs a code sent to your own registered mobile: `POST /api/v1/search/{id}/otp` with { "otp": "123456" } (4 to 8 digits). Not waiting returns 409 not_waiting.

## Cancel

`DELETE /api/v1/search/{id}` cancels an unfinished search. A finished one returns 409 already_finished.

## Idempotency and repeats

| Situation | Charge | Result |
| --- | --- | --- |
| Same Idempotency-Key within 24 hours | Free | Original returned, header Idempotent-Replayed: true, HTTP 200 |
| Same search again by the same person within 24 hours | Free | Same id, replayed: true |
| Same search by a colleague in the same workspace within 24 hours | Charged | Served from the recent result with a new id |

Idempotency-Key: 1 to 255 printable ASCII characters, no spaces. Reuse with a different body returns 409 idempotency_key_reused.

## List

`GET /api/v1/search?limit=50&offset=0&status=succeeded&batch_id=...`, newest first, limit at most 200.
