AckdbIngestRequest

Request body for data ingestion from workflow tools. Content is validated against the source's content_schema, traits against trait_definitions.

Properties

sourcestringrequired

Source type (must exist in schema registry).

contentobjectrequired

The content payload with text and source-specific metadata.

textstringrequired

Raw text from the source - this is what gets embedded and searched.

metadataobjectrequired

Source-specific fields, validated against the source's content_schema.

identityLinksarray ofrequired

At least one required. Links this data to an entity via external IDs.

traitsarray of

Pre-computed trait values. Each is validated against trait_definitions.

entityIdstring | null

Explicit entity ID. If provided and no identity match, uses this entity.

contactIdstring

Actor 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.

personIdstring

Actor 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>required

When this data was created in the source system (not when ingested).

idempotencyKeystring

Optional 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 of

Optional 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 of

Optional 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

AckdbIngestRequest 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": {}
    }
  ]
}