Ingest
Every write endpoint: push events, create records, fix, remove, undo.
Events
One event: validated against its source pack, committed whole or refused whole naming the field.
{
"source": "stripe",
"content": {
"text": "Invoice INV-4021 paid, $12,400",
"metadata": { "event_type": "invoice.paid", "amount_paid_cents": 1240000 }
},
"identityLinks": [{ "externalIdType": "domain", "externalIdValue": "piedpiper.com" }],
"timestamp": "2026-08-11T09:14:02Z"
}curl -s -X POST "$ACKDB_URL/ingest" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "source": "stripe", "content": { "text": "Invoice INV-4021 paid, $12,400", "metadata": { "event_type": "invoice.paid", "amount_paid_cents": 1240000 } }, "identityLinks": [{ "externalIdType": "domain", "externalIdValue": "piedpiper.com" }], "timestamp": "2026-08-11T09:14:02Z" }'{
"data": {
"entityId": "01HXYZ123456789ABCDEFGH",
"contentId": "01HXYZ123456789ABCDEFGI",
"created": true,
"identityLinksCreated": 1,
"traitsWritten": 1
}
}- Resolution, first hit wins:
domainlink (matches or creates) → source ids (match only) → universal ids → mintable anchor → explicitentityId→ unresolved pool. A matching link beatsentityId. Identity. - Three non-error outcomes: created,
deduplicated: true(idempotency key already stored — keys are global, not per company),unresolvedContentId(nothing matched). - Static facts never go through ingest — attributes refuse with 422. Build.
- Field-by-field walkthrough: Push events.
Batch ingest (up to 500 items)
{
"source": "slack",
"items": [
{
"content": {
"text": "…",
"metadata": {}
},
"timestamp": "2026-08-01T00:00:00Z",
"idempotencyKey": "…",
"contactId": "01KWSVN7YY7Q2Q9QZ8GEHHYW14",
"personId": "01KWSVN7YY7Q2Q9QZ8GEHHYW14"
}
],
"identityLinks": [
{
"entityId": "01HXYZ123456789ABCDEFGH",
"source": "slack",
"externalIdType": "slack_channel_id",
"externalIdValue": "C04ABC",
"confidence": 1,
"linkedAt": "2026-08-01T00:00:00Z"
}
],
"entityId": "…",
"traits": [
{
"traitKey": "slack_message_count",
"value": 42,
"op": "increment",
"workflow": "slack-ingestion",
"confidence": 1
}
],
"contacts": [
{
"contactId": "01JQC7X8Y9Z0A1B2C3D4E5F6G7",
"email": "…",
"name": "…",
"title": "…",
"phone": "…",
"department": "…",
"linkedinUrl": "…",
"isExternal": true,
"identityLinks": [
{
"externalIdType": "slack_user_id",
"externalIdValue": "U07ABC123"
}
],
"traits": [
{
"traitKey": "slack_message_count",
"value": 42,
"op": "increment",
"workflow": "slack-ingestion",
"confidence": 1
}
]
}
]
}curl -s -X POST "$ACKDB_URL/ingest/batch" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "source": "slack", "items": [ { "content": { "text": "…", "metadata": {} }, "timestamp": "2026-08-01T00:00:00Z", "idempotencyKey": "…", "contactId": "01KWSVN7YY7Q2Q9QZ8GEHHYW14", "personId": "01KWSVN7YY7Q2Q9QZ8GEHHYW14" } ], "identityLinks": [ { "entityId": "01HXYZ123456789ABCDEFGH", "source": "slack", "externalIdType": "slack_channel_id", "externalIdValue": "C04ABC", "confidence": 1, "linkedAt": "2026-08-01T00:00:00Z" } ], "entityId": "…", "traits": [ { "traitKey": "slack_message_count", "value": 42, "op": "increment", "workflow": "slack-ingestion", "confidence": 1 } ], "contacts": [ { "contactId": "01JQC7X8Y9Z0A1B2C3D4E5F6G7", "email": "…", "name": "…", "title": "…", "phone": "…", "department": "…", "linkedinUrl": "…", "isExternal": true, "identityLinks": [ { "externalIdType": "slack_user_id", "externalIdValue": "U07ABC123" } ], "traits": [ { "traitKey": "slack_message_count", "value": 42, "op": "increment", "workflow": "slack-ingestion", "confidence": 1 } ] } ] }'{
"data": {
"entityId": "…",
"created": true,
"accepted": 0,
"duplicated": 0,
"total": 0,
"identityLinksCreated": 0,
"traitsWritten": 0,
"contactsProcessed": 0
}
}Batch differs from single ingest: no unresolved fallback (422 instead), no files[], one bad item rejects all with its 0-based index.
Query ingest logs
curl -s "$ACKDB_URL/ingest/logs?limit=50&offset=0" \
-H "Authorization: Bearer $ACKDB_API_KEY"{
"data": [
{
"id": "…",
"endpoint": "/ingest",
"source": "slack",
"status": "success",
"entityId": "…",
"entityCreated": true,
"resolutionPath": "domain_match",
"idempotencyKey": "…",
"requestSizeBytes": 0,
"contentId": "…",
"eventId": "…",
"identityLinksCreated": 0,
"traitsWritten": 0,
"contactsProcessed": 0
}
],
"pagination": {
"total": 0,
"limit": 0,
"offset": 0
}
}Records
Create a company (direct record insert)
{
"domain": "acme.com",
"name": "Acme Corp",
"actor": "vansh@floqer.com"
}curl -s -X POST "$ACKDB_URL/entities" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "domain": "acme.com", "name": "Acme Corp", "actor": "vansh@floqer.com" }'{
"data": {
"entityId": "01JZK3...",
"name": "Acme Corp",
"domain": "acme.com",
"anchor": {
"type": "domain",
"value": "acme.com"
},
"created": false
}
}Create or upsert a contact (person-first)
{
"email": "elly@acme.com",
"name": "Elly Chen",
"title": "VP Engineering",
"phone": "…",
"department": "…",
"linkedinUrl": "https://www.linkedin.com/in/ellychen",
"avatarUrl": "https://media.licdn.com/dms/image/v2/abc/profile.jpg",
"source": "…",
"identityLinks": [
{
"externalIdType": "…",
"externalIdValue": "…"
}
],
"traits": [
{}
]
}curl -s -X POST "$ACKDB_URL/entities/{entityId}/contacts" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "elly@acme.com", "name": "Elly Chen", "title": "VP Engineering", "phone": "…", "department": "…", "linkedinUrl": "https://www.linkedin.com/in/ellychen", "avatarUrl": "https://media.licdn.com/dms/image/v2/abc/profile.jpg", "source": "…", "identityLinks": [ { "externalIdType": "…", "externalIdValue": "…" } ], "traits": [ {} ] }'{
"data": "…"
}Create a person by hand
{
"name": "Elly Chen",
"avatarUrl": "https://media.licdn.com/dms/image/v2/abc/profile.jpg",
"keys": [
{
"externalIdType": "email_address",
"externalIdValue": "elly@newco.com"
}
]
}curl -s -X POST "$ACKDB_URL/persons" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Elly Chen", "avatarUrl": "https://media.licdn.com/dms/image/v2/abc/profile.jpg", "keys": [ { "externalIdType": "email_address", "externalIdValue": "elly@newco.com" } ] }'{
"data": "…"
}List unresolved content
curl -s "$ACKDB_URL/unresolved" \
-H "Authorization: Bearer $ACKDB_API_KEY"{
"data": [
{
"id": "01HXYZ123456789ABCDEFGH",
"source": "slack",
"identityLinks": [
{
"externalIdType": "domain",
"externalIdValue": "C04ABC",
"confidence": 1
}
],
"text": "…",
"metadata": {},
"traits": [
{
"traitKey": "slack_message_count",
"value": 42,
"op": "increment",
"workflow": "slack-ingestion",
"confidence": 1
}
],
"rawPayload": {},
"timestamp": "2026-08-01T00:00:00Z",
"createdAt": "2026-08-01T00:00:00Z"
}
]
}Resolve unresolved content to an entity
{
"unresolvedContentId": "01HXYZ123456789ABCDEFGH",
"entityId": "01HXYZ123456789ABCDEFGI"
}curl -s -X POST "$ACKDB_URL/entities/resolve" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "unresolvedContentId": "01HXYZ123456789ABCDEFGH", "entityId": "01HXYZ123456789ABCDEFGI" }'{
"data": {
"entityId": "01HXYZ123456789ABCDEFGI",
"contentId": "01HXYZ123456789ABCDEFGJ",
"created": false,
"identityLinksCreated": 1,
"traitsWritten": 2
}
}On resolve, entityId: null always mints a new company — look the id up first with GET /entities/lookup and pass the match.
Fix
Merge two duplicate entities
{
"winnerId": "01HXYZ123456789ABCDEFGH",
"loserId": "01HABCD123456789EFGHIJK"
}curl -s -X POST "$ACKDB_URL/entities/merge" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "winnerId": "01HXYZ123456789ABCDEFGH", "loserId": "01HABCD123456789EFGHIJK" }'{
"data": {
"winnerId": "01HXYZ123456789ABCDEFGH",
"loserId": "01HABCD123456789EFGHIJK",
"linksTransferred": 2,
"traitsTransferred": 3,
"contactsTransferred": 1,
"eventsTransferred": 42,
"rawPayloadsTransferred": 42,
"contentTransferred": 5,
"filesTransferred": 0,
"conversationsTransferred": 3
}
}Find and merge all duplicate entities
{
"apply": true
}curl -s -X POST "$ACKDB_URL/entities/merge-duplicates" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "apply": true }'{
"data": {
"message": "…",
"duplicateGroups": 0,
"applied": true
}
}Merge two persons
{
"loserId": "01HABCD123456789EFGHIJK",
"winnerOverride": "…"
}curl -s -X POST "$ACKDB_URL/persons/{personId}/merge" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "loserId": "01HABCD123456789EFGHIJK", "winnerOverride": "…" }'{
"data": "…"
}Batch-merge the open candidates (dry-run by default)
{
"dryRun": true,
"limit": 0
}curl -s -X POST "$ACKDB_URL/persons/merge-duplicates" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "dryRun": true, "limit": 0 }'{
"data": "…"
}Note
The two bulk merges arm differently: companies execute on { "apply": true }, persons on { "dryRun": false }. Humans are never merged automatically — conflicts wait in GET /persons/merge-candidates.
Declaratively set all values of one link type on a company
{
"values": [
"123456789",
"987654321"
]
}curl -s -X PUT "$ACKDB_URL/entities/{ref}/links/{type}" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "values": [ "123456789", "987654321" ] }'{
"data": {
"externalIdType": "duns_number",
"values": [
"123456789",
"987654321"
]
}
}Re-parent a mis-attached contact to the right company
{
"entityId": "01J8ZE4X1M5Y6Z7A8B9C0D1E2F",
"reason": "…"
}curl -s -X POST "$ACKDB_URL/contacts/{contactId}/move" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "entityId": "01J8ZE4X1M5Y6Z7A8B9C0D1E2F", "reason": "…" }'{
"data": "…"
}Re-point a contact to another person (the split primitive)
{
"personId": "…"
}curl -s -X PATCH "$ACKDB_URL/contacts/{contactId}" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "personId": "…" }'{
"data": "…"
}domain is read-only on the links routes (422): it is the record's anchor, managed by resolution and merge. Ingest-time link conflicts never error — first writer wins and the event still lands.
Remove & undo
Ids only, by design — no filter-based mass delete. Every door takes dryRun, reason, actor.
Delete specific events by id (with cascade)
{
"ids": [
"01J9ABCDEF0123456789ABCDEF",
"01J9ZYXWVU9876543210ZYXWVU"
],
"dryRun": true,
"reason": "duplicate Slack import"
}curl -s -X DELETE "$ACKDB_URL/data/events" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "ids": [ "01J9ABCDEF0123456789ABCDEF", "01J9ZYXWVU9876543210ZYXWVU" ], "dryRun": true, "reason": "duplicate Slack import" }'{
"data": {
"dryRun": true,
"deleted": {
"events": 2,
"rawPayloads": 2,
"files": 1,
"contentChunks": 3,
"conversations": 1,
"blobs": 1
},
"wouldDelete": {
"events": 1,
"rawPayloads": 1,
"files": 0,
"contentChunks": 2,
"conversations": 1,
"blobs": 0
},
"resolved": [
{
"id": "…",
"entityId": "…",
"source": "…",
"eventType": "…",
"summary": "…",
"timestamp": "2026-08-01T00:00:00Z",
"linkedRawPayloadId": "…",
"fileCount": 0
}
],
"notFound": [
"…"
],
"retained": [
{
"kind": "identity_links",
"entityId": "…",
"count": 0,
"reason": "…"
}
],
"pendingRebuild": [
{
"entityId": "…",
"source": "…",
"contentChunksDeleted": 0,
"conversationsDeleted": 0,
"payloadsQueuedForReembed": 0
}
],
"rebuildNote": "…"
}
}Delete a company and its entire timeline
{
"reason": "…",
"actor": "…",
"dryRun": true
}curl -s -X DELETE "$ACKDB_URL/entities/{entityId}" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "…", "actor": "…", "dryRun": true }'{
"data": {
"dryRun": true,
"deletionId": "…",
"kind": "company",
"record": {
"id": "…",
"label": "…"
},
"deleted": {
"events": 0,
"rawPayloads": 0,
"files": 0,
"contentChunks": 0,
"conversations": 0,
"contacts": 0,
"persons": 0,
"entities": 0,
"identityLinks": 0,
"contactIdentityLinks": 0,
"personIdentityLinks": 0,
"traits": 0,
"memberAssignments": 0,
"mergeCandidates": 0
},
"detached": {
"events": 0,
"agentSessions": 0
},
"tombstone": {
"active": true,
"identifiers": [
{
"type": "…",
"value": "…"
}
]
},
"retained": [
{
"kind": "…",
"count": 0,
"reason": "…"
}
],
"pendingRebuild": [
{}
]
}
}Delete a human (company timelines keep their evidence)
{
"reason": "…",
"actor": "…",
"dryRun": true
}curl -s -X DELETE "$ACKDB_URL/persons/{personId}" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "…", "actor": "…", "dryRun": true }'{
"data": {
"dryRun": true,
"deletionId": "…",
"kind": "company",
"record": {
"id": "…",
"label": "…"
},
"deleted": {
"events": 0,
"rawPayloads": 0,
"files": 0,
"contentChunks": 0,
"conversations": 0,
"contacts": 0,
"persons": 0,
"entities": 0,
"identityLinks": 0,
"contactIdentityLinks": 0,
"personIdentityLinks": 0,
"traits": 0,
"memberAssignments": 0,
"mergeCandidates": 0
},
"detached": {
"events": 0,
"agentSessions": 0
},
"tombstone": {
"active": true,
"identifiers": [
{
"type": "…",
"value": "…"
}
]
},
"retained": [
{
"kind": "…",
"count": 0,
"reason": "…"
}
],
"pendingRebuild": [
{}
]
}
}Bulk delete records by id
{
"kind": "company",
"ids": [
"…"
],
"dryRun": true,
"reason": "…",
"actor": "…"
}curl -s -X DELETE "$ACKDB_URL/data/records" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "kind": "company", "ids": [ "…" ], "dryRun": true, "reason": "…", "actor": "…" }'{
"data": {
"dryRun": true,
"kind": "company",
"requested": 0,
"deletedCount": 0,
"failedCount": 0,
"notFound": [
"…"
],
"results": [
{
"ref": "…",
"status": "deleted",
"result": {},
"error": {
"code": "…",
"message": "…"
}
}
]
}
}Purge all ingested data
curl -s -X DELETE "$ACKDB_URL/data/purge?source=stripe" \
-H "Authorization: Bearer $ACKDB_API_KEY"{
"data": {
"message": "Purged all data",
"source": "stripe",
"deleted": {
"entities": 8,
"contacts": 24,
"contactIdentityLinks": 24,
"contactTraits": 48,
"identityLinks": 36,
"traits": 112,
"events": 112,
"rawPayloads": 112,
"entityContent": 112,
"unresolvedContent": 0,
"unresolvedContacts": 0,
"conversations": 0,
"files": 0,
"ingestQueue": 0
}
}
}Deleted is moved, not destroyed — every removed row lands in the archive:
List the deletion archive (recently deleted)
curl -s "$ACKDB_URL/deletions?limit=50" \
-H "Authorization: Bearer $ACKDB_API_KEY"{
"data": [
{
"id": "…",
"kind": "company",
"recordId": "…",
"label": "…",
"reason": "…",
"actor": "…",
"status": "deleted",
"tombstoneActive": true,
"identifiers": [
{
"type": "…",
"value": "…"
}
],
"counts": {
"events": 0,
"rawPayloads": 0,
"files": 0,
"contentChunks": 0,
"conversations": 0,
"contacts": 0,
"persons": 0,
"entities": 0,
"identityLinks": 0,
"contactIdentityLinks": 0,
"personIdentityLinks": 0,
"traits": 0,
"memberAssignments": 0,
"mergeCandidates": 0
},
"detached": {
"events": 0,
"agentSessions": 0
},
"createdAt": "2026-08-01T00:00:00Z",
"restoredAt": "2026-08-01T00:00:00Z",
"purgedAt": "2026-08-01T00:00:00Z"
}
],
"nextCursor": "…"
}Restore a deleted record
curl -s -X POST "$ACKDB_URL/deletions/{id}/restore" \
-H "Authorization: Bearer $ACKDB_API_KEY"Permanently purge the archived copy (the tombstone stays)
curl -s -X DELETE "$ACKDB_URL/deletions/{id}" \
-H "Authorization: Bearer $ACKDB_API_KEY"Lift the tombstone (allow re-creation)
curl -s -X DELETE "$ACKDB_URL/deletions/{id}/tombstone" \
-H "Authorization: Bearer $ACKDB_API_KEY"A record delete freezes its ids as a tombstone: a re-syncing pipeline can't re-mint it — ingest drops or skips and says so in data.tombstoned; deliberate creates get 409 TOMBSTONED_IDENTIFIER. Delete and undo.
Team & ownership
Create a member
{
"email": "…",
"name": "…",
"title": "…",
"team": "…",
"managerId": "…",
"notes": "…",
"sourceIds": [
{
"source": "…",
"externalIdType": "…",
"externalIdValue": "…"
}
]
}curl -s -X POST "$ACKDB_URL/members" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "…", "name": "…", "title": "…", "team": "…", "managerId": "…", "notes": "…", "sourceIds": [ { "source": "…", "externalIdType": "…", "externalIdValue": "…" } ] }'{
"data": "…"
}Assign (or reassign) a member to a company
{
"memberId": "…",
"email": "…",
"name": "…",
"title": "…",
"isPrimary": true,
"source": "…"
}curl -s -X POST "$ACKDB_URL/entities/{id}/members" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "memberId": "…", "email": "…", "name": "…", "title": "…", "isPrimary": true, "source": "…" }'{
"data": "…"
}Get a company's primary owner (DRI)
curl -s "$ACKDB_URL/entities/{id}/owner" \
-H "Authorization: Bearer $ACKDB_API_KEY"{
"data": "…"
}Transfer a member's book to another member
{
"toMemberId": "…",
"toEmail": "…",
"name": "…",
"title": "…",
"entityIds": [
"…"
],
"contactIds": [
"…"
],
"all": true,
"source": "…"
}curl -s -X POST "$ACKDB_URL/members/{id}/transfer" \
-H "Authorization: Bearer $ACKDB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "toMemberId": "…", "toEmail": "…", "name": "…", "title": "…", "entityIds": [ "…" ], "contactIds": [ "…" ], "all": true, "source": "…" }'{
"data": "…"
}An unowned account reads as data: null — a coverage gap, not an error. Deactivation is PATCH /members/{id} with isActive: false.