AckdbIngestRequest
Request body for data ingestion from workflow tools. Content is validated against the source's content_schema, traits against trait_definitions.
Properties
sourcestringrequiredSource type (must exist in schema registry).
contentobjectrequiredThe content payload with text and source-specific metadata.
textstringrequiredRaw text from the source - this is what gets embedded and searched.
metadataobjectrequiredSource-specific fields, validated against the source's content_schema.
identityLinksarray ofrequiredAt least one required. Links this data to an entity via external IDs.
traitsarray ofPre-computed trait values. Each is validated against trait_definitions.
entityIdstring | nullExplicit entity ID. If provided and no identity match, uses this entity.
contactIdstringActor rail (#326). Pins this event's actor to an EXISTING contact - the person↔company affiliation edge - and, implicitly, to that contact's person: `contact_id` + `person_id` on the stored event are stamped from it, overriding whatever the event type's contact mappings would have inferred from `content.metadata`. NEVER mints: the contact must already exist. Honored ONLY on person-scope event types. Rejected with 422 when: the resolved event type is not person-scope (or declares no scope); the value is not a 26-character ULID; no contact has that id; the contact belongs to a DIFFERENT entity than the one this request resolved to; the request resolved to no entity at all (send a `domain` identity link or `entityId`); or it contradicts `personId` (the contact's person is not that person). A durable person key extracted from the payload that is owned by ANOTHER human is a 409 + a merge candidate - keys are never re-pointed. The rail also folds into the auto-computed idempotency key: the same content pinned to a different actor is a different write, not a duplicate.
personIdstringActor rail (#326). Pins this event's actor to an EXISTING person (the human). Unlike `contactId` it does not assert the edge: affiliation resolution then runs normally for the resolved company, so the person↔company edge MAY be created (this is the sanctioned cross-company path - the same human showing up at a new account). A merged (tombstoned) person id canonicalizes to its survivor. NEVER mints a person. Honored ONLY on person-scope event types. Rejected with 422 when: the resolved event type is not person-scope; the value is not a 26-character ULID; no person has that id; the request resolved to no entity; or it contradicts `contactId`. Same 409 + merge-candidate rule as `contactId` for a payload key owned by another human, and the same idempotency-key fold.
timestampstring <date-time>requiredWhen this data was created in the source system (not when ingested).
idempotencyKeystringOptional caller-provided idempotency key. If omitted, ackDB auto-computes one via SHA-256(source + content). Duplicate keys are silently accepted (200 with deduplicated: true). Use this to pass a natural key like a Stripe event ID or Slack message timestamp.
contactsarray ofOptional people to create or update at this entity, in addition to whatever the event's contact mappings extract. This is how a multi-person event (a meeting's attendee list, a group thread) records everyone: the contactId/personId rails above pin ONE actor, this array carries the rest. Each entry resolves independently through the same person-first resolver.
filesarray ofOptional files to upload and attach to this entity. Each file is base64-encoded. Max 10MB per file (configurable via FILE_MAX_SIZE_BYTES). Files are stored in the configured FileStore and processed asynchronously by `npm run process-files` to generate embeddings for semantic search.
Example
{
"source": "slack",
"content": {
"text": "Customer asked about pricing",
"metadata": {}
},
"identityLinks": [
{
"externalIdType": "domain",
"externalIdValue": "C04ABC",
"confidence": 1
}
],
"traits": [
{
"traitKey": "slack_message_count",
"value": 42,
"op": "increment",
"workflow": "slack-ingestion",
"confidence": 1
}
],
"entityId": null,
"contactId": "01KWSVN7YY7Q2Q9QZ8GEHHYW14",
"personId": "01KWSVN7YY7Q2Q9QZ8GEHHYW14",
"timestamp": "2026-03-08T10:00:00Z",
"idempotencyKey": "stripe:evt_1234567890",
"contacts": [
{
"contactId": "01JQC7X8Y9Z0A1B2C3D4E5F6G7",
"email": "string",
"name": "string",
"title": "string",
"phone": "string",
"department": "string",
"linkedinUrl": "string",
"isExternal": false,
"identityLinks": [
{
"externalIdType": "slack_user_id",
"externalIdValue": "U07ABC123"
}
],
"traits": [
{
"$ref": "See AckdbIngestTrait"
}
]
}
],
"files": [
{
"name": "q4-proposal.pdf",
"type": "application/pdf",
"sizeBytes": 2048576,
"content": "string",
"sourceFileId": "F04ABC123",
"sourceUrl": "https://files.slack.com/files-pri/T04ABC-F04ABC123/q4-proposal.pdf",
"metadata": {}
}
]
}