> ## Documentation Index
> Fetch the complete documentation index at: https://docs.norbelys.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Merge duplicate custom fields

> Merge a duplicate custom field into a canonical field without overwriting data. Source values copy only into empty targets; identical duplicates and blank source values are removed. Different non-empty values remain on the source and prevent it from being archived. Both fields must be active and have the same type.



## OpenAPI

````yaml /openapi.json post /fields/{id}/merge
openapi: 3.1.1
info:
  description: >-
    The **Norbelys API** is a single, predictable REST surface for cold email
    and

    outreach — people, senders, programs, and sending all live behind the five

    patterns below. Developer-first and AI-first: every name is either already

    invented (Schema.org) or obvious.


    ## Authentication


    Every request authenticates with an **org-scoped API key**. Create one in

    **Settings → API keys** and send it as a bearer token:


    ```http

    GET https://api.norbelys.com/v1/people

    Authorization: Bearer ak_live_…

    ```


    Interactive agents may instead use OAuth 2.1 (see `/auth.md` and the

    `/.well-known/oauth-protected-resource` metadata).


    ## Conventions


    - **Base URL** — `https://api.norbelys.com/v1`.

    - **JSON in, JSON out.** Timestamps are ISO-8601 in UTC.

    - **Cursor pagination.** List endpoints take `limit` + `cursor` and return
      `{ data, hasMore, nextCursor }` (offset-paged tables add `page` + `total`).
    - **Expansions.** Detail GETs take an `expand[]` query param to inline
    related
      data (e.g. `GET /people/{id}?expand[]=timeline`) instead of extra calls.
    - **Soft deletes.** Anything that has been used is archived, never
    hard-deleted —
      `DELETE` archives the resource and returns it.

    ## Errors


    Failures return the same envelope on every 4xx/5xx, with the matching HTTP
    status:


    ```json

    { "error": { "type": "invalid_request", "code": "invalid_param",
                "message": "…", "hint": "…", "doc_url": "…" } }
    ```


    `type` is a broad, machine-routable category derived from the status; `code`
    is the

    stable machine contract you branch on (never the human `message`). See the
    `ApiError`

    schema.


    ## Idempotency


    Every `POST` accepts an optional **`Idempotency-Key`** header. Reuse the
    same key to

    replay the original result for 24h instead of re-executing — so a retried
    create can

    never double-charge or duplicate a record.


    ## Rate limits & versioning


    Abuse control is enforced at the edge; responses advertise the policy via
    the

    `RateLimit-Policy` header, and a `429` carries `Retry-After`. The API is
    versioned in

    the URL path (`/v1`). Breaking changes ship under a new version; a retiring
    surface is

    announced with `Deprecation` + `Sunset` response headers at least 90 days
    ahead.
  title: Norbelys API
  version: 0.0.1
  x-api-lifecycle:
    currentVersion: v1
    deprecationPolicyUrl: https://docs.norbelys.com/conventions#versioning
    deprecationSignals:
      - Deprecation header
      - Sunset header
    minNoticeDays: 90
    versioning: url-path
servers:
  - description: Production
    url: https://api.norbelys.com/v1
security:
  - bearerAuth: []
tags:
  - description: Your workspace — profile and onboarding state.
    name: Organization
  - description: >-
      The people you reach out to: create, import, segment, and read a person's
      timeline.
    name: People
  - description: >-
      Custom person fields — the tenant-defined attributes that ride on every
      person under `customFields`.
    name: Fields
  - description: >-
      Static lists of people — hand-curated audiences you add to and remove
      from.
    name: Groups
  - description: >-
      Saved audience filters — dynamic rule-trees evaluated live against your
      people.
    name: Segments
  - description: >-
      Connected mailboxes that send your email — the sending identity behind
      each program.
    name: Senders
  - description: >-
      Every domain concern in one place: sending domains (verification, DNS
      records, daily caps) and DMARC monitoring (collection addresses, ingested
      reports).
    name: Domains
  - description: >-
      Campaigns and sequences — with their steps, variants and enrollments
      nested underneath.
    name: Programs
  - description: >-
      Send email through the unified send door, read the unified sent/received
      log, and fetch a message's exact sent source.
    name: Messages
  - description: >-
      The one analytics door: named catalog queries (funnels, feeds, health,
      A/B, DMARC) over the event store.
    name: Analytics
  - description: Addresses excluded from sending — unsubscribes, bounces, and complaints.
    name: Suppressions
  - description: >-
      Connected tools (Slack, CRMs): mint a Connect session, configure
      notifications, disconnect.
    name: Integrations
  - description: >-
      Synchronous email verification — deliverability, reachability, and
      identity signals for an address.
    name: Verify
  - name: Files
paths:
  /fields/{id}/merge:
    post:
      tags:
        - Fields
      summary: Merge duplicate custom fields
      description: >-
        Merge a duplicate custom field into a canonical field without
        overwriting data. Source values copy only into empty targets; identical
        duplicates and blank source values are removed. Different non-empty
        values remain on the source and prevent it from being archived. Both
        fields must be active and have the same type.
      operationId: fields.merge
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            description: The duplicate/source field id whose values should be migrated.
            examples:
              - fld_legacy_company
        - description: >-
            Opt-in idempotency for a safely retried write: reuse the same key to
            replay the original result for 24h instead of re-executing.
            Recommended on every POST.
          in: header
          name: Idempotency-Key
          required: false
          schema:
            maxLength: 255
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FieldMergeParams'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FieldMergeResult'
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
          description: The request was malformed or failed validation.
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
          description: Missing or invalid credentials.
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
          description: Authenticated, but not permitted.
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
          description: No such resource (or it has been archived).
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
          description: The request conflicts with the resource's current state.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
          description: Well-formed but semantically invalid.
        '429':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
          description: Rate limit exceeded — retry after the `Retry-After` interval.
        '500':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
          description: An unexpected error on our side.
      x-codeSamples:
        - label: TypeScript / JavaScript
          lang: typescript
          source: >-
            import { createClient } from "@norbelys/sdk";


            const norbelys = createClient({ apiKey: process.env.NORBELYS_API_KEY
            });


            const { data, error } = await norbelys.fields.merge("fld_3n7p1q", {
            targetId: "fld_canonical_company" });

            if (error) {
              throw new Error(error.error.message);
            }

            console.log(data);
        - label: Python
          lang: python
          source: >-
            import norbelys


            config = norbelys.Configuration(host="https://api.norbelys.com/v1",
            access_token="ak_live_…")

            with norbelys.ApiClient(config) as client:
                api = norbelys.FieldsApi(client)
                result = api.fields_merge("fld_3n7p1q", { "targetId": "fld_canonical_company" })
                print(result)
        - label: Go
          lang: go
          source: >-
            cfg := norbelys.NewConfiguration()

            cfg.Servers = norbelys.ServerConfigurations{{URL:
            "https://api.norbelys.com/v1"}}

            client := norbelys.NewAPIClient(cfg)

            ctx := context.WithValue(context.Background(),
            norbelys.ContextAccessToken, "ak_live_…")


            result, _, err := client.FieldsAPI.FieldsMerge(ctx,
            "fld_3n7p1q").Execute()
        - label: Ruby
          lang: ruby
          source: >-
            require "norbelys"


            Norbelys.configure { |c| c.access_token = ENV["NORBELYS_API_KEY"] }

            api = Norbelys::FieldsApi.new

            result = api.fields_merge("fld_3n7p1q", { targetId:
            "fld_canonical_company" })

            puts result
        - label: CLI
          lang: bash
          source: >-
            norbelys fields merge fld_3n7p1q --data '{ "targetId":
            "fld_canonical_company" }'
        - label: curl
          lang: bash
          source: |-
            curl -X POST "https://api.norbelys.com/v1/fields/{id}/merge" \
              -H "Authorization: Bearer $NORBELYS_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{ "targetId": "fld_canonical_company" }'
components:
  schemas:
    FieldMergeParams:
      type: object
      properties:
        targetId:
          type: string
          description: The canonical destination field id.
          examples:
            - fld_canonical_company
      required:
        - targetId
      title: FieldMergeParams
    FieldMergeResult:
      type: object
      properties:
        alreadyEqual:
          type: integer
          minimum: 0
          description: >-
            Contacts whose identical source value was removed because the target
            already contained it.
        conflicts:
          type: integer
          minimum: 0
          description: >-
            Contacts with two different non-empty values. Both values were
            preserved and the source field remains active.
        contactsScanned:
          type: integer
          minimum: 0
          description: Contacts that had the source key when the merge began.
        copied:
          type: integer
          minimum: 0
          description: >-
            Contacts whose source value was copied into an empty target and then
            removed from the source key.
        defaultConflict:
          type: boolean
          description: >-
            Whether source and target define different non-null defaults. A
            conflict keeps the source field active.
        discardedEmpty:
          type: integer
          minimum: 0
          description: >-
            Contacts whose source key was removed because its value was null or
            blank.
        object:
          const: field_merge
          default: field_merge
          description: Always "field_merge" — the resource discriminator.
        source:
          $ref: '#/components/schemas/Field'
          description: The source field after the merge.
        sourceArchived:
          type: boolean
          description: >-
            Whether the source was archived. This is true only when no source
            values or default conflict remain.
        sourceValuesRemaining:
          type: integer
          minimum: 0
          description: >-
            Contacts that still carry the source key, normally because its value
            conflicts with the target.
        target:
          $ref: '#/components/schemas/Field'
          description: The canonical target field after the merge.
      required:
        - alreadyEqual
        - conflicts
        - contactsScanned
        - copied
        - defaultConflict
        - discardedEmpty
        - source
        - sourceArchived
        - sourceValuesRemaining
        - target
      title: FieldMergeResult
    ApiError:
      properties:
        error:
          properties:
            code:
              description: >-
                Stable machine code — branch on this, never on the human
                `message`.
              enum:
                - invalid_param
                - missing_param
                - invalid_expand
                - duplicate_email
                - already_exists
                - unauthorized
                - forbidden
                - mailbox_already_connected
                - suppression_protected
                - program_not_launchable
                - database_unavailable
                - rate_limited
                - idempotency_key_reuse
                - idempotency_in_progress
              type: string
            doc_url:
              description: Optional link to the relevant documentation.
              format: uri
              type: string
            hint:
              description: Optional one-sentence remediation.
              type: string
            message:
              description: Human-readable explanation. Never a contract.
              type: string
            type:
              description: Broad, machine-routable category derived from the HTTP status.
              examples:
                - invalid_request
                - authentication_error
                - rate_limit
              type: string
          required:
            - type
            - code
            - message
          title: ApiErrorDetail
          type: object
      required:
        - error
      title: ApiError
      type: object
    Field:
      type: object
      properties:
        archivedAt:
          description: When the field was archived (RFC 3339), or null if still active.
        code:
          type: string
          description: The merge tag and storage key, templated as `{{ code }}`.
          examples:
            - favorite_category
        createdAt:
          description: When the field was created (RFC 3339).
        defaultValue:
          $ref: '#/components/schemas/FieldDefaultValue'
        id:
          type: string
          description: The field id.
          examples:
            - fld_tz4a98xat96iws9zmbrgj3a
        isFilterable:
          type: boolean
          description: Whether this field is offered as a segment/list filter.
          examples:
            - true
        label:
          type: string
          description: The human-readable display name for the field.
          examples:
            - Favorite category
        object:
          const: field
          default: field
          description: Always "field" — the resource discriminator.
        options:
          anyOf:
            - type: array
              items:
                type: string
            - type: 'null'
          description: Allowed values for a `dropdown` field; null for other types.
          examples:
            - - Electronics
              - Apparel
              - Home
        order:
          type: integer
          minimum: 0
          description: Display order among custom fields (lower = first).
          examples:
            - 0
        type:
          $ref: '#/components/schemas/FieldType'
        updatedAt:
          description: When the field was last updated (RFC 3339).
      required:
        - code
        - defaultValue
        - id
        - isFilterable
        - label
        - options
        - order
        - type
      title: Field
    FieldDefaultValue:
      anyOf:
        - type: string
          maxLength: 1000
        - type: number
        - type: 'null'
      title: FieldDefaultValue
      description: >-
        The value applied to new/imported contacts that omit this field, or null
        when disabled.
    FieldType:
      enum:
        - text
        - email
        - phone
        - date
        - datetime
        - number
        - dropdown
      type: string
      title: FieldType
      description: >-
        The data type: `text`, `email`, `phone`, `date`, `datetime`, `number`,
        or `dropdown`. `email` and `phone` values are validated on write: a
        phone is normalized to E.164, and an invalid value is dropped without
        failing the request.
      examples:
        - dropdown
  securitySchemes:
    bearerAuth:
      description: >-
        Org-scoped Norbelys API key. Create one in Settings → API keys and send
        it as `Authorization: Bearer ak_…`.
      scheme: bearer
      type: http

````