---
title: "Declare a custom source"
method: POST
path: "/api/v1/ackdb/schemas/sources"
tags: ["ackDB"]
---

# Declare a custom source

`POST /api/v1/ackdb/schemas/sources`

## Request

- Base URL: `http://localhost:3009`
- URL: `http://localhost:3009/api/v1/ackdb/schemas/sources`
- Auth: HTTP bearer

Declares a new custom source definition at runtime. A source declared here is INDISTINGUISHABLE from a shipped pack: it flows through the same DB tables, so ingest, discovery, agent guidance, contact extraction, and embedding all treat it identically.

`eventTypeField` is REQUIRED (no static single-event sources) and `identityLinkTypes` MUST include `domain` so the source can resolve to or create entities. Default trait keys MUST be namespaced as `<sourceType>_*`. Use `?dryRun=true` to validate the payload without persisting anything.

## Query parameters

- `dryRun` boolean

## Request body

- AckdbSourceCreateRequest: Declare a custom source with eventTypeField. identityLinkTypes may be empty for a closed source containing only person_only types; otherwise include domain.
  - `sourceType` string, required: Lowercase source identifier (e.g. `app_usage`).
  - `displayName` string, required: Human-readable name.
  - `description` string, required: What data this source carries.
  - `version` string: Optional source version string.
  - `queryingGuide` string: Optional agent-facing guidance on how to query this source's events.
  - `identityLinkTypes` string[], required: May be empty when every declared event is person_only and allowCustomEventTypes is false. Otherwise must include domain. Add domain before adding company event types or allowing custom types.
  - `eventTypeField` string, required: REQUIRED. Which content.metadata field determines the event type.
  - `eventTypePrefix` string, required: Namespace prefix for event types.
  - `defaultContentNature` 'conversational' | 'structured' | 'activity-signal': Optional default content nature applied to events that don't specify one.
  - `allowCustomEventTypes` boolean: Optional HubSpot-custom-objects escape hatch: accept unknown event types and skip per-event validation.
  - `defaultTraits` AckdbSourceCreateDefaultTrait[]: Optional default traits (each key must be `<sourceType>_*`).
    - `traitKey` string, required: Trait identifier, namespaced as `<sourceType>_*`.
    - `valueType` 'string' | 'number' | 'boolean' | 'date' | 'string_array', required: Type system for trait validation and storage. Controls how trait values are validated on ingest and stored in the entity_traits table.
    - `description` string: Human/AI-readable description of the trait.
    - `allowedValues` string[]: Optional enum constraint.
  - `events` AckdbEventTypeInput[]: Optional event type definitions to create alongside the source.
    - `eventType` string, required: Event type identifier (e.g. `usage.daily_rollup`).
    - `description` string, required: What this event represents.
    - `category` string: Optional cross-source activity category.
    - `rationale` string: Optional: why this event is included.
    - `contentNature` 'conversational' | 'structured' | 'activity-signal', required: How the event's content should be treated downstream (e.g. for embedding).
    - `eventScope` 'person' | 'entity' | 'person_only', required: entity: company; person: person at a company; person_only: independent human owner. Create a separate type to change ownership. Personal types support structured/activity content only.
    - `fields` object, required: Field name → field definition. The event's content schema.
    - `promotedMetadataFields` string[]: Fields promoted into entity_events.metadata for fast filtering/aggregation.
    - `traitHints` object[]: Optional traits that should be updated when this event is ingested.
      - `traitKey` string, required: Trait to update. Must be a registered trait definition.
      - `computeHint` string: How to compute the trait value from this event's content.
      - `updateStrategy` 'always' | 'first_only' | 'latest': Advisory authoring hint only - it does NOT auto-translate to an ingest `op`. `always` ≈ `op:set`; `latest` ≈ `op:set_if_newer` (date traits); `first_only` has no native op (the workflow must guard on first observation). The workflow still sets the actual `op` on each ingested trait.
    - `ingestExample` object: Optional complete POST /ingest template for this event.
    - `replacesEvents` string[]: Source events intentionally excluded because this event covers their signal.
    - `contactMapping` AckdbContactMappingInput: Maps event content fields to a contact (person) extracted at ingest time. A durable external ID is required; display name, email, title, and other person properties are optional.
      - `role` string, required: Contact's role for this mapping (e.g. `user`, `attendee`).
      - `nameField` string: Optional content field holding the contact's display name. Omit when a durable person key such as LinkedIn URL is the only person data this source owns.
      - `emailField` string: Content field holding the contact's email.
      - `titleField` string: Content field holding the contact's job title.
      - `externalIdField` string, required: Content field holding the contact's external ID value.
      - `externalIdType` string, required: External ID type for the contact (e.g. `app_user_email`).
      - `linkedinUrlField` string: Content field holding the contact's LinkedIn URL.
      - `phoneField` string: Content field holding the contact's phone number.
      - `departmentField` string: Content field holding the contact's department.
      - `excludeWhen` object: Skip extracting this contact when these field/value conditions match.
      - `startedAtField` string: Content field holding when the EMPLOYMENT began (#345) — a fact, never inferred from ingest order.
      - `endedAtField` string: Content field holding when the employment ENDED (#345); a value flips the edge to `former`.
      - `externalIdScope` 'person' | 'affiliation': What `externalIdField` identifies: the HUMAN (a durable person key) or the AFFILIATION (a workspace-local seat id, match-only). Default `affiliation`.
      - `endsPrior` boolean: This event asserts the human's OTHER affiliations have ended (job-change semantics): their other active edges flip to `former` with a `contact.ended` each. Person-scope event types only. Default false.
      - `isPrimary` boolean: The resolved edge becomes the person's PRIMARY affiliation (#544, `primarySource: ingest`; sticky while that edge is active). Applied only when the edge is ACTIVE. Person-scope event types only. Default false.
    - `contactMappings` AckdbContactMappingInput[]: Multiple contact mappings (merged with the singular `contactMapping`).
      - `role` string, required: Contact's role for this mapping (e.g. `user`, `attendee`).
      - `nameField` string: Optional content field holding the contact's display name. Omit when a durable person key such as LinkedIn URL is the only person data this source owns.
      - `emailField` string: Content field holding the contact's email.
      - `titleField` string: Content field holding the contact's job title.
      - `externalIdField` string, required: Content field holding the contact's external ID value.
      - `externalIdType` string, required: External ID type for the contact (e.g. `app_user_email`).
      - `linkedinUrlField` string: Content field holding the contact's LinkedIn URL.
      - `phoneField` string: Content field holding the contact's phone number.
      - `departmentField` string: Content field holding the contact's department.
      - `excludeWhen` object: Skip extracting this contact when these field/value conditions match.
      - `startedAtField` string: Content field holding when the EMPLOYMENT began (#345) — a fact, never inferred from ingest order.
      - `endedAtField` string: Content field holding when the employment ENDED (#345); a value flips the edge to `former`.
      - `externalIdScope` 'person' | 'affiliation': What `externalIdField` identifies: the HUMAN (a durable person key) or the AFFILIATION (a workspace-local seat id, match-only). Default `affiliation`.
      - `endsPrior` boolean: This event asserts the human's OTHER affiliations have ended (job-change semantics): their other active edges flip to `former` with a `contact.ended` each. Person-scope event types only. Default false.
      - `isPrimary` boolean: The resolved edge becomes the person's PRIMARY affiliation (#544, `primarySource: ingest`; sticky while that edge is active). Applied only when the edge is ACTIVE. Person-scope event types only. Default false.
  - `contextOnly` boolean: This source's events describe a PERSON but anchor to a company (`job` → the employer, `education` → the school), so the entities it mints are context rather than accounts (#493). A company whose only ingested evidence comes from such sources is hidden from the Companies grain and excluded from every score population. NULLABLE: `null` means never-decided and lets the pack loader seed it; `true`/`false` is a decision the loader will not overwrite.

## Response `200`

Dry-run validation succeeded (nothing persisted)

- object
  - `data` object, required
    - `valid` true, required
    - `dryRun` true, required
    - `sourceType` string, required
    - `eventTypes` string[], required

## Other responses

- `201`: Source created, event types and default traits registered
- `400`: Validation failed (bad shape, missing `domain` link, non-namespaced trait, etc.)
- `409`: A source with this `sourceType` already exists (use PUT to edit)

---

[API](https://skmtc.dev/floqer/apis/floqer-public-api.md) - [All operations](https://skmtc.dev/floqer/apis/floqer-public-api/llms.txt) - [OpenAPI document](https://skmtc.dev/floqer/apis/floqer-public-api/revisions/fce50dc07b20?raw)
