Dataset sync
Read the cited dataset incrementally — cursors, updated_since, per-cell citations
GET /v1/blueprints/{blueprint_id}/entities/{entity_key} is the canonical
read: the cross-run, canonical rows of one entity, with citations. Entity
keys come from GET /v1/blueprints/{blueprint_id}, and the ids from
GET /v1/blueprints (discover, don't
guess).
curl -s "$BASE_URL/v1/blueprints/$BP/entities/companies?limit=50&receipts=true" \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY"{"data": [{"id": "…", "updated_at": "2026-08-03T11:02:44+00:00",
"name": "Acme KK", "hq_city": "Osaka",
"citations": {"hq_city": {"value": "Osaka", "confidence": 0.93,
"evidence_id": "…",
"source_urls": ["https://…"],
"retrieved_at": "2026-08-01T…"}}}],
"has_more": true, "next_cursor": "MjAyNi0wOC0wMy4uLg",
"entity": {"key": "companies", "columns": [{"name": "name", "type": "text"}]},
"blueprint": {"domain_name": "…", "entities": ["companies"]}}Parameters:
limit(≤200) +cursor— keyset pagination ordered by(updated_at, id)ascending. Stable while rows are inserted.updated_since=<iso-timestamp>— the sync primitive: only rows changed since your last sync. Store your high-water mark (or the final cursor) and poll on your own schedule — or let thedataset.updatedwebhook tell you when.fields=name,hq_city— column subset.receipts=true— nest per-cell citations. This is the differentiator; use it.
Reading a citation honestly
Each cell carries the same verdict the app shows, not just a number:
| Field | Meaning |
|---|---|
verdict | supported · leans-supported · uncertain · corrected · contradicted · unverifiable · not checked |
confidence | 0–1 float, or null when the cell was never graded |
cited | false when the cell has no source URL — never present it as sourced |
verdict_note | the QA agent's short justification |
source_urls, evidence_id, retrieved_at | provenance |
stated | for a converted quantity, the value and unit exactly as the source stated them before conversion ({"value": "5.0", "unit": "mi"} beside a stored 8.05 km); value is a decimal string, so the source's precision survives. null when nothing was converted |
beside | rows recorded beside the cell, newest first; empty when there are none. Each has a kind: also_stated (another value the same run stated for the cell), not_applied (a value later research found and did not apply; reason says why, in plain words) withheld (found only outside the run's allowed sources, so it has no value), non_conforming (a value that broke the column's rule) or replaced (a value the cell held until a newer one replaced it, kept as history). Each also carries its own verdict, source_hosts, note and captured_at |
A contradicted or cited: false cell is a signal to surface to a human, not
a fact to act on.
The citation is always the source of the value the cell holds. Its value,
verdict and source_urls belong to that value. A row in beside, however new,
does not replace it: read a beside row's verdict as that row's, never as the
cell's. The rows are there so you see what the app's popover shows: a value the
same run also stated, or a newer finding that was not applied.
Preview caps (unsubscribed accounts)
Free-preview accounts receive only the 10 preview rows per entity: the
best-evidenced rows, in the order of the cross-run report (the same 10 a saved report
tab previews), which can change as research re-checks them. When the cap
withholds rows, the response says so explicitly — "truncated": true,
"preview_row_cap": 10, plus has_more: true — and paging past the cap
requires a subscription. A table that fits within the cap is returned whole, with
truncated: false; exports say the same in their metadata and X-Truncated
header. Never report a truncated preview as the complete dataset.
Merges, renames and successor links
Rows can change identity after you have read them. A person may mark two rows
as the same thing (a merge), and a rerun may find that a record was renamed
(renamed: renamed in place, the old name kept as an alias) or replaced by
another record (succeeded_by: the two are linked). GET /v1/changes lists
all three, newest first, in one feed:
curl -s "$BASE_URL/v1/changes?live=true" -H "Authorization: Bearer $DEEPSIEVE_API_KEY"{"data": [{"object": "change", "id": "…", "kind": "merge", "entity": "companies",
"record_id": "…", "by": "user", "survivor_name": "Clover",
"merged_names": ["Clover Network"], "undoable": true, "undo_blocked": null,
"created_at": "…", "undone_at": null, "blueprint": {"id": "…", "name": "…"}}],
"has_more": false, "next_cursor": null, "blueprint": {"id": "…", "name": "…"}}Filter with entity, kind, by (user, automatic, or operator for merges
DeepSieve's own dedup made) and live; page
with limit and cursor. A person's rename through Report Chat is listed too,
by: "user". If you
sync by record id, this is how you learn that a row you hold was folded into
another or renamed. The feed is ordered by when each change was made, so an
UNDO does not add a new item at the top: it sets undone_at on the original
one, wherever it sits. To catch undos, re-read the feed from the start rather
than only its first page.
POST /v1/changes/{change_id}/undo undoes one, exactly as the app's Undo does:
a merge brings the rows back with their evidence, a rename restores the old
name, and a link is removed. Researched values stay. It changes your customer's
data, so an agent acting for a person should confirm first. It needs
data:write and the member role; a viewer is refused whatever the key carries.
Send an Idempotency-Key so a retry returns the first result, not a 409.
Removing a wrong link
A record's many-to-many links (for example, the people a record lists) can be
wrong. POST /v1/links/remove takes one linked record out of a record's links,
as a person does in the app. entity is the side whose record lists the link;
to_entity is the linked entity.
curl -s -X POST $BASE_URL/v1/links/remove \
-H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"blueprint_id": "…", "entity": "<entity>", "record_id": "…", "to_entity": "<linked entity>", "target_id": "…"}'Research does not add that link back until the removal is undone. If it was the
record's primary link, the next link becomes primary. The answer is the
link_removed change with its change_id, and the same change appears in
GET /v1/changes with kind: "link_removed". Its link object says what was
removed: record_name, to_entity, target_id, target_name, was_primary
(an undo makes it primary again) and removed_via (session for a person in the
app; api_key or oauth for an agent). POST /v1/changes/{change_id}/undo puts
the link back, at its old position when that still fits.
The removal is reversible, so there is no confirmation step, but it changes your
customer's data: tell your human what you removed. A link the record does not
have (already removed, or never linked) is 409 link_not_present; an unknown
entity, record or relationship is 404 not_found. It needs data:write and the
member role. The MCP tool is remove_link, and the CLI is
deepsieve links remove.
Cells a person set on purpose
A value a person set on purpose (in chat, or by applying newer research) wins over
research. A rerun never overwrites it; what research finds since is kept beside
it. When you sync cell citations from GET /v1/data, a held cell's citation
carries hold:
{"hold": {"source": "chat_edit", "set_at": "…",
"pending_finding": {"evidence_id": "…", "value": "J. Austen",
"source_urls": ["https://…"], "found_at": "…",
"stage": "extract"}}}GET /v1/holds?pending=true lists every held cell with a pending finding.
POST /v1/cells/{cell_id}/apply with {"finding_evidence_id": "…"} takes the
finding (the cell stays protected); if research found something newer since,
it is 409 finding_changed and nothing is written. POST /v1/cells/{cell_id}/keep,
with the same body, keeps your human's value and dismisses the finding: research does
not raise that value again unless it cites a new page. POST /v1/cells/{cell_id}/release
stops protecting the cell, and research updates it on its next pass. All three change
your customer's data and need data:write and the member role.
Cells stated more than one way
When one research run states a cell more than one way, the cell shows the better-supported value, and the citation says which and why:
{"value": "Jane Austen", "evidence_id": "…",
"selection": {"state": "conflicting",
"reason": "the quality check supported 2 different values; …"},
"beside": [{"kind": "also_stated", "evidence_id": "…", "value": "J. Austen",
"verdict": "supported", "choosable": true, "refused_by": []}]}selection.state is picked (the run chose), repicked (the quality check
confirmed or moved the choice), conflicting (it supported two or more values:
a person should choose) or chosen (a person chose); null when the run
stated the cell one way. GET /v1/conflicts lists every conflicting cell with
the values to choose from. POST /v1/cells/{cell_id}/choose with
{"evidence_id": "…", "expected_value_evidence_id": "…"} shows one of them; if
the cell changed since your human looked, it is 409 choice_changed and nothing
is written. The cell is then held, so later research keeps a different value
beside it, and the choice is a choice change in GET /v1/changes, undone with
POST /v1/changes/{change_id}/undo. A sync that mirrors the citation sees the
value change and selection.state become chosen.
Marking rows as duplicates
POST /v1/merges/preview shows what merging rows would do, and writes nothing.
POST /v1/merges merges them, as the app's "Mark as duplicate" does: the
survivor keeps its row, the merged rows' values, citations and child rows move
to it, and later runs fold the merged names into it. Two rows' lists of links are
combined, never a tie (links_combined in the preview).
Send the preview's preview_digest with the merge: it applies exactly the merge
you previewed. If anything changed since (a conflict decided differently, a tie's
values changed, a cell newly filled), it is 409 merge_changed, nothing is
written, and error.details.preview is the merge as it stands now, with its own
digest to send once you have checked it. To merge without a preview on purpose,
send apply_without_preview: true; sending neither is 422 preview_required. The
response's preview_checked says which.
curl -s -X POST $BASE_URL/v1/merges -H "Authorization: Bearer $DEEPSIEVE_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"entity": "companies", "survivor_id": "<uuid>", "loser_ids": ["<uuid>"],
"choices": {"hq_city::<loser uuid>": "loser"},
"preview_digest": "<preview_digest from the preview>"}'Where the rows disagree, the better-evidenced value wins. Where the evidence is
equal it is a tie, and nobody has decided it: the merge is refused
409 ties_unresolved until you send a choice for each tie (the preview lists
them as unresolved_ties), or accept_survivor_on_ties: true. With three or
more rows, one choice can change the conflicts after it, so preview again with
your choices until unresolved_ties is empty. Every check runs inside the merge
itself: a refused merge writes nothing. Rows of an entity whose names are unique
per parent, sitting under different parents, are refused 409 merge_across_parents
naming the parents: merge only within one parent. The response's
change_id undoes the merge with POST /v1/changes/{change_id}/undo.
A tie you settle is a value set on purpose: later research does not overwrite it
(it is stored beside it, as "Newer research found X"). A value a person set on
purpose in either row wins over research in the merge (basis: "held"). If a
cell the merge filled changes afterwards, its undo is a 409 naming the cell;
re-send with {"overwrite_changes": true} to undo anyway.
Bulk export
GET /v1/export?format=ndjson|json|csv[&entity=][&blueprint_id=] — NDJSON is
the agent default: line 1 is a metadata record with the Blueprint schema, then
one record per line with citations. CSV is per-entity and drops citations.
blueprint_id exports a specific Blueprint by id (leaves the active one
unchanged); omitted, it exports the active one. The old name, workspace_id,
still works until its sunset date and says so in Deprecation/Sunset headers
and a warnings entry in the metadata record; see the
changelog.
Each NDJSON and JSON record carries _identity: where the row came from and
went, with ids, so a sync that holds rows by id can follow them.
{"_identity": {"formerly": ["Clover Network"],
"succeeded_by": null,
"succeeded_from": [{"id": "…", "name": "Old Imprint"}],
"merged_from": ["Clover Inc"]}}formerly is the names a rerun renamed the row from, succeeded_by the record
that replaced it, succeeded_from the records it replaced, and merged_from the
rows a person merged into it. Every field is present on every row. Undone
changes are left out, only a record's newest link counts, and a successor that
was later merged away is followed to the row it was merged into. Names are as
they were at the time of the change. A free-preview export references only rows
it contains.
Free-preview (unsubscribed) accounts are capped to the preview rows of each
table on both surfaces (the same best-evidenced rows a saved report tab previews) — flagged by
truncated + preview_row_cap. A row outside the preview is 402 payment_required, and a row can move in or out of it as research re-checks it.