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

# Bulk-update programs

> Apply ONE change to MANY campaigns at once, in a single call. Pass EITHER `action` — a lifecycle verb (archive, restore, pause, resume) — OR `patch`, the settings to set on all of them (pacing: sendIntervalMinutes / jitterMinSeconds / jitterMaxSeconds / newPerDay; sending window: windowFrom / windowTo / activeDays / timezone; tracking; auto-pause thresholds). Use this whenever the same change goes to several campaigns (e.g. 'archive every campaign', 'pause them all', 'spread the sends out across the day') INSTEAD of calling archive/pause/resume/update once per campaign — one call means one confirmation for the operator instead of one per campaign. It cannot rename campaigns, edit their emails, or change their mailbox pool. Returns how many changed plus any per-campaign errors.



## OpenAPI

````yaml /openapi.json post /programs/bulk
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:
  /programs/bulk:
    post:
      tags:
        - Programs
      summary: Bulk-update programs
      description: >-
        Apply ONE change to MANY campaigns at once, in a single call. Pass
        EITHER `action` — a lifecycle verb (archive, restore, pause, resume) —
        OR `patch`, the settings to set on all of them (pacing:
        sendIntervalMinutes / jitterMinSeconds / jitterMaxSeconds / newPerDay;
        sending window: windowFrom / windowTo / activeDays / timezone; tracking;
        auto-pause thresholds). Use this whenever the same change goes to
        several campaigns (e.g. 'archive every campaign', 'pause them all',
        'spread the sends out across the day') INSTEAD of calling
        archive/pause/resume/update once per campaign — one call means one
        confirmation for the operator instead of one per campaign. It cannot
        rename campaigns, edit their emails, or change their mailbox pool.
        Returns how many changed plus any per-campaign errors.
      operationId: programs.bulk
      parameters:
        - 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/BulkProgramsInput'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkProgramsResult'
        '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.programs.bulk({ ids:
            ["prg_b2n4x8mq1w7e9rk3v5tz0yc6"] });

            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.ProgramsApi(client)
                result = api.programs_bulk({ "ids": ["prg_b2n4x8mq1w7e9rk3v5tz0yc6"] })
                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.ProgramsAPI.ProgramsBulk(ctx).Execute()
        - label: Ruby
          lang: ruby
          source: >-
            require "norbelys"


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

            api = Norbelys::ProgramsApi.new

            result = api.programs_bulk({ ids: ["prg_b2n4x8mq1w7e9rk3v5tz0yc6"]
            })

            puts result
        - label: CLI
          lang: bash
          source: >-
            norbelys programs bulk --data '{ "ids":
            ["prg_b2n4x8mq1w7e9rk3v5tz0yc6"] }'
        - label: curl
          lang: bash
          source: |-
            curl -X POST "https://api.norbelys.com/v1/programs/bulk" \
              -H "Authorization: Bearer $NORBELYS_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{ "ids": ["prg_b2n4x8mq1w7e9rk3v5tz0yc6"] }'
components:
  schemas:
    BulkProgramsInput:
      type: object
      properties:
        action:
          enum:
            - archive
            - pause
            - restore
            - resume
          type: string
          description: >-
            Lifecycle verb to apply to every listed campaign. Omit when sending
            a `patch`.
          examples:
            - archive
        ids:
          type: array
          minItems: 1
          maxItems: 200
          items:
            type: string
          description: The campaign ids to act on (1–200 per call).
          examples:
            - - prg_b2n4x8mq1w7e9rk3v5tz0yc6
        patch:
          type: object
          properties:
            activeDays:
              anyOf:
                - type: array
                  items:
                    type: integer
                    minimum: 0
                    maximum: 6
                - type: 'null'
              description: Weekdays sends are allowed (0=Sun..6=Sat).
            addUnsubscribe:
              type: boolean
              description: >-
                Attach RFC 8058 one-click unsubscribe headers + footer. Defaults
                off (opt-in).
            autoPauseOnCompanyReply:
              type: boolean
              description: Pause every lead at a company once one of them replies.
            bouncePauseThreshold:
              type: number
              minimum: 0
              maximum: 100
              description: >-
                Auto-pause when the bounce rate reaches this percent; 0
                disables. Default 2.
              examples:
                - 2
            jitterMaxSeconds:
              type: integer
              minimum: 0
              description: Random extra wait per send, upper bound (seconds).
              examples:
                - 60
            jitterMinSeconds:
              type: integer
              minimum: 0
              description: Random extra wait per send, lower bound (seconds).
              examples:
                - 30
            newPerDay:
              type: integer
              minimum: 0
              description: >-
                New leads started per day for the whole campaign, shared across
                all its mailboxes (follow-ups excluded); 0 = uncapped.
              examples:
                - 25
            plainTextMode:
              type: boolean
              description: >-
                Send the body as plain text only: no HTML part, open pixel, or
                link tracking.
            sendIntervalMinutes:
              type: integer
              minimum: 3
              description: Minutes between sends from one mailbox (cold-email floor is 3).
              examples:
                - 5
            spamPauseThreshold:
              type: number
              minimum: 0
              maximum: 100
              description: >-
                Auto-pause when the spam-complaint rate reaches this percent; 0
                disables. Default 0.3.
              examples:
                - 0.3
            stopCondition:
              enum:
                - reply
                - click
                - open
                - none
              type: string
              description: >-
                Engagement that pulls a lead out of the cadence: `reply`,
                `click`, `open`, `none`.
            timezone:
              type: string
              description: IANA timezone.
              examples:
                - America/Chicago
            timezoneSource:
              enum:
                - sender
                - recipient
              type: string
              description: >-
                Evaluate the send window in the campaign timezone (`sender`,
                default) or each person's timezone (`recipient`).
              examples:
                - recipient
            trackClicks:
              type: boolean
              description: Rewrite links to track clicks.
            trackingDomainId:
              anyOf:
                - type: string
                - type: 'null'
              description: >-
                Optional campaign-level tracking hostname override; null
                resolves automatically per sender domain.
            trackOpens:
              type: boolean
              description: Insert the open pixel.
            unsubscribeText:
              type: string
              description: Custom unsubscribe footer copy.
            windowFrom:
              anyOf:
                - type: string
                  pattern: ^([01]?\d|2[0-3]):[0-5]\d$
                - type: 'null'
              description: Daily window start, "HH:MM".
              examples:
                - '09:00'
            windowTo:
              anyOf:
                - type: string
                  pattern: ^([01]?\d|2[0-3]):[0-5]\d$
                - type: 'null'
              description: Daily window end, "HH:MM".
              examples:
                - '17:00'
          description: >-
            Settings to set on EVERY listed campaign (pacing, window, tracking,
            auto-pause guards). Omit when sending an `action`.
      required:
        - ids
      title: BulkProgramsInput
    BulkProgramsResult:
      type: object
      properties:
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ProgramBulkError'
          description: Per-campaign failures — every other campaign still applied.
        object:
          const: bulk_result
          description: Always "bulk_result". Identifies the result type.
        programs:
          type: array
          items:
            $ref: '#/components/schemas/Program'
          description: The resulting campaigns.
        updated:
          type: integer
          minimum: 0
          description: How many campaigns were changed.
          examples:
            - 17
      required:
        - errors
        - object
        - programs
        - updated
      title: BulkProgramsResult
    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
    ProgramBulkError:
      type: object
      properties:
        id:
          type: string
          description: The campaign that failed.
        message:
          type: string
          description: Why it failed.
      required:
        - id
        - message
      title: ProgramBulkError
    Program:
      type: object
      properties:
        activeDays:
          anyOf:
            - type: array
              items:
                type: integer
                minimum: 0
                maximum: 6
            - type: 'null'
          description: Weekdays sends are allowed (0=Sun..6=Sat).
          examples:
            - - 1
              - 2
              - 3
              - 4
              - 5
        addUnsubscribe:
          type: boolean
          description: Attach RFC 8058 one-click unsubscribe headers + footer.
        archivedAt:
          description: When it was archived, or null if live.
        autoPauseOnCompanyReply:
          type: boolean
          description: Pause every lead at a company once one of them replies.
        bouncePauseThreshold:
          type: number
          minimum: 0
          maximum: 100
          description: Auto-pause when the bounce rate reaches this percent; 0 disables.
          examples:
            - 2
        createdAt:
          description: Creation instant (RFC 3339).
        id:
          type: string
          description: The program id.
          examples:
            - prg_b2n4x8mq1w7e9rk3v5tz0yc6
        jitterMaxSeconds:
          type: integer
          minimum: 0
          description: Random extra wait per send, upper bound (seconds).
          examples:
            - 60
        jitterMinSeconds:
          type: integer
          minimum: 0
          description: Random extra wait per send, lower bound (seconds).
          examples:
            - 30
        kind:
          enum:
            - cold_sequence
          type: string
          description: Kind of sending; `cold_sequence` is the only kind today.
          examples:
            - cold_sequence
        name:
          type: string
          description: Display name.
          examples:
            - Q3 outbound — founders
        newPerDay:
          type: integer
          minimum: 0
          description: >-
            New leads started per day for the WHOLE campaign, shared across all
            its mailboxes (follow-ups are not counted). 0 = no daily cap — new
            leads flow at mailbox capacity. Real throughput is always bounded by
            the mailboxes' own daily limits.
          examples:
            - 25
        object:
          const: program
          default: program
          description: Always "program" — the resource discriminator.
        pausedAt:
          description: When it was paused (RFC 3339), or null.
        pausedMeta:
          anyOf:
            - $ref: '#/components/schemas/ProgramPauseMeta'
            - type: 'null'
          description: >-
            For an auto-pause, the numbers behind it (measured rate vs
            threshold); null for a manual pause or no pause.
        pausedReason:
          anyOf:
            - enum:
                - manual
                - bounce_rate
                - spam_rate
                - orphaned_sender
              type: string
            - type: 'null'
          description: >-
            Why it's paused, or null: `manual` (an operator paused it),
            `bounce_rate` or `spam_rate` (an auto circuit-breaker tripped),
            `orphaned_sender` (it lost its last live mailbox).
          examples:
            - bounce_rate
        plainTextMode:
          type: boolean
          description: >-
            Send the body as plain text only: no HTML part, open pixel, or link
            tracking.
        senderTags:
          anyOf:
            - type: array
              items:
                type: string
            - type: 'null'
          description: >-
            Dynamic mailbox selector: senders carrying ANY of these tags are
            auto-attached to (and auto-detached from) the campaign as their tags
            change. Manual attaches are unaffected. null = manual assignment
            only.
          examples:
            - - acme-pool
        sendIntervalMinutes:
          type: integer
          minimum: 0
          description: Minutes between sends from one mailbox.
          examples:
            - 5
        spamPauseThreshold:
          type: number
          minimum: 0
          maximum: 100
          description: >-
            Auto-pause when the spam-complaint rate reaches this percent; 0
            disables.
          examples:
            - 0.3
        startDate:
          description: Scheduled first-send instant (RFC 3339), or null.
          examples:
            - '2026-07-01T14:00:00.000Z'
        status:
          enum:
            - draft
            - active
            - paused
            - done
          type: string
          description: 'Lifecycle status: `draft`, `active`, `paused`, `done`.'
          examples:
            - active
        stopCondition:
          enum:
            - reply
            - click
            - open
            - none
          type: string
          description: >-
            Engagement that pulls a lead out of the cadence: `reply`, `click`,
            `open`, `none`.
          examples:
            - reply
        timezone:
          type: string
          description: IANA timezone the window applies in.
          examples:
            - America/Chicago
        timezoneSource:
          enum:
            - sender
            - recipient
          type: string
          description: >-
            Evaluate the sending window in the campaign timezone (`sender`) or
            each person's timezone (`recipient`, falling back to the campaign
            timezone when absent).
          examples:
            - sender
        trackClicks:
          type: boolean
          description: Rewrite links to track clicks.
        trackingDomainId:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Optional campaign-level tracking hostname override; null resolves
            automatically per sender domain.
        trackOpens:
          type: boolean
          description: Insert the open pixel.
        unsubscribeText:
          anyOf:
            - type: string
            - type: 'null'
          description: Custom unsubscribe footer copy.
        updatedAt:
          description: Last-update instant (RFC 3339).
        variantDistribution:
          enum:
            - pattern
            - random
          type: string
          description: >-
            How A/B arms are spread across leads with no decided winner:
            `pattern` (deterministic per-lead hash) or `random` (independent
            roll per lead).
          examples:
            - pattern
        windowFrom:
          anyOf:
            - type: string
            - type: 'null'
          description: Daily window start, "HH:MM".
          examples:
            - '09:00'
        windowTo:
          anyOf:
            - type: string
            - type: 'null'
          description: Daily window end, "HH:MM".
          examples:
            - '17:00'
      required:
        - activeDays
        - addUnsubscribe
        - autoPauseOnCompanyReply
        - bouncePauseThreshold
        - id
        - jitterMaxSeconds
        - jitterMinSeconds
        - kind
        - name
        - newPerDay
        - pausedMeta
        - pausedReason
        - plainTextMode
        - senderTags
        - sendIntervalMinutes
        - spamPauseThreshold
        - status
        - stopCondition
        - timezone
        - timezoneSource
        - trackClicks
        - trackingDomainId
        - trackOpens
        - unsubscribeText
        - variantDistribution
        - windowFrom
        - windowTo
      title: Program
    ProgramPauseMeta:
      type: object
      properties:
        rate:
          type: number
          minimum: 0
          maximum: 100
          description: The measured rate that tripped the pause, as a percent.
          examples:
            - 6.7
        threshold:
          type: number
          minimum: 0
          maximum: 100
          description: The threshold it crossed, as a percent.
          examples:
            - 2
      required:
        - rate
        - threshold
      title: ProgramPauseMeta
  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

````