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

# Bulk-import contacts from JSON

> Create or update up to 1000 contacts from JSON rows and record them as one Import History batch: the JSON twin of `POST /v1/contacts/import-csv`. Rows take the batch-create shape (`email`, `firstName`, `lastName`, `subscribed`, `customFields`, `consent`) and follow its rules: a missing or invalid email, or an address repeated in the batch, fails its row (`errors[]`, `207`) while the others land. New contacts are subscribed unless the row says `subscribed: false`; re-subscribing a contact who opted out keeps its other fields and adds a `RESUBSCRIBE_SKIPPED` warning.

The batch is listed in Import History under `fileName` (optional), and each contact it writes carries the returned `importId` until a later import touches it (filter on it to list them; deleting the import in the app removes the contacts it created). `POST /v1/contacts` with `contacts[]` writes the same rows without an Import History entry, for recurring syncs.

Two opt-in modes. `dryRun: true` returns the response this import would return (`summary`, `errors`, row `warnings`, `fieldsCreated` as fields it would create) with `dryRun: true` and no `importId`, and writes nothing: no contacts, field definitions or Import History entry; `validate` is not run and nothing is charged. Insert vs update is decided by the same address match the write uses, so a real run right after returns the same counts. `onConflict: "skip"` only creates missing contacts: each existing contact is left unchanged (an opt-out row included), counted in `summary.skipped` with a `CONTACT_EXISTS_SKIPPED` warning naming its `row`. Either way `inserted + updated + failed + skipped = total`. A dry run cannot foresee failures that only happen while writing (a concurrent change, a database refusal), so the real run can still fail where the preview succeeded. With `validate: true`, inline checks are still metered per submitted row, rows `skip` left alone included; a request that leaves no address to check is not charged.



## OpenAPI

````yaml /api-reference/openapi-public-v1.yaml post /v1/contacts/import
openapi: 3.1.0
info:
  title: Brew Public API v1
  version: 1.0.0
  description: >-
    Brew Public API v1. Generated from the app Zod contracts
    (`lib/<domain>/contracts.ts`): this document is the contract, and every
    operation documents exactly the error codes it can return.


    - Base URL `https://brew.new/api`; every path starts with `/v1`; JSON in and
    out. Request bodies and query strings are strict: an unknown key is `400
    INVALID_REQUEST` and `error.param` names it.

    - Identity is always in the path. `GET /v1/<collection>` lists (`{ data,
    pagination: { limit, cursor, hasMore } }`; page until `cursor` is `null`),
    and `GET /v1/<collection>/{id}` returns the bare resource, the same object a
    write returns. `?include=` expands relations on a detail read only, and is
    `400` on a list. There is no `?<idKey>=` read anywhere.

    - Writes carry identity in the path and return the bare resource: `POST
    /v1/<collection>` creates (`201`, or `202` when work continues
    asynchronously), `PATCH` and `DELETE /v1/<collection>/{id}` update or delete
    (`DELETE` answers `{ <idField>, deleted }` and is idempotent). Lifecycle
    state changes such as publishing ride `PATCH` as attributes. Two writes
    report more than the row and say so in their shape: `POST` and `PATCH
    /v1/contacts` are upserts that answer `{ contact, created | updated, … }`
    because whether a row was created, which fields changed, and which field
    definitions were minted are facts about the write, not about the contact.

    - Non-CRUD operations are explicit action sub-paths, for example `POST
    /v1/automations/{automationId}/test`, `POST /v1/domains/{domainId}/verify`,
    `POST /v1/sends/{sendId}/cancel` and `POST
    /v1/automations/triggers/{triggerEventId}/fire`.

    - Every resource has one root. Sends read and write at `/v1/sends`, fired
    triggers at `/v1/automations/trigger-instances`, and `/v1/analytics/*` holds
    only reports (aggregates over a window). Every endpoint answers in the same
    envelopes, with no exceptions.

    - Field names are camelCase; enum values are lowercase snake_case;
    timestamps are ISO 8601 UTC strings. Identifiers are opaque strings of at
    most 64 characters (`triggerEventId` up to 256); never parse a prefix.

    - Everything that runs shares one status vocabulary: `queued`, `scheduled`,
    `running`, `paused`, `completed`, `partially_completed`, `failed`,
    `canceled`. It covers sends, automation runs, manual audience runs, audience
    builds and inbox placement tests, so one status switch reads them all. A
    step inside a run is `running`, `completed`, `failed` or `skipped`.

    - Every error on every endpoint is `{ error: { code, type, message,
    suggestion, docs, param?, retryAfter?, details? } }`. Branch on `code`: the
    closed list is the `ApiErrorCode` component and every operation documents
    exactly the codes it can return. Non-fatal caveats arrive as `warnings: [{
    code, message, field? }]` on `2xx` bodies.

    - A credential is brand-scoped (the brand is implicit; send nothing) or
    organization-scoped (name the brand with the `X-Brand-Id` header on
    brand-scoped operations, else `400 BRAND_ID_REQUIRED`; there is no default
    brand). Only `POST /v1/api-keys` carries a `brandId` field, and only a
    signed-in organization admin session may call `/v1/api-keys`. A brand
    outside your reach surfaces as `404`, never `403`.

    - Send `Idempotency-Key` (up to 100 characters) on any POST you might
    repeat: the same key and body replays the original response for 24 hours;
    the same key with a different body is `409 IDEMPOTENCY_CONFLICT`. Rate
    limits are per credential and named policy (`X-RateLimit-*` headers; `429`
    carries `Retry-After`). Credit-metered operations advertise `402` and carry
    `x-brew-credited: true` in the spec.

    - Discovery without a key: `GET /v1/help` (JSON catalog with scopes, credits
    and rate limits per operation), `GET /v1/llms.txt` (this guide) and `GET
    /v1/health`. The OpenAPI document is served at `/openapi.json`.
  contact:
    name: Brew Support
    url: https://docs.brew.new
    email: support@brew.new
servers:
  - url: https://brew.new/api
    description: Production
  - url: http://localhost:3000/api
    description: Local development
security:
  - bearerAuth: []
  - apiKeyAuth: []
tags:
  - name: Emails
    description: >-
      Email designs and sending. Generate a design with the Brew email agent,
      edit, version, restore — then send it: `POST /v1/sends` delivers a design
      to a target (a saved audience, an inline list, or a single address) via a
      verified domain, and `POST /v1/sends` with `test: true` fires a one-off
      test. Sending is not campaign-specific. Send reads live under Sends
      (`/v1/sends`).
  - name: Sends
    description: >-
      The unit of delivery and analytics. `POST /v1/sends` delivers a design to
      a target; `GET /v1/sends` lists campaign sends (with lifetime stats) or,
      with a join filter, one automation’s per-recipient deliveries; `GET
      /v1/sends/{sendId}` reads one send and its events. Cancel, pause and
      resume are action sub-paths.
  - name: Brands
    description: >-
      Brand lifecycle for ORGANIZATION-scoped credentials: list the brands a
      credential can reach, read one, and create a new one (extraction runs
      asynchronously — poll `GET /v1/brands/{brandId}` until `status:
      completed`). These endpoints act on the organization, so they take no
      `X-Brand-Id`.
  - name: Analytics
    description: >-
      Read-only cross-resource reports: the brand overview, windowed automation
      performance and the unified event feed. Send rows (with lifetime stats)
      live at `/v1/sends`; fired triggers at
      `/v1/automations/trigger-instances`.
  - name: Automations
    description: >-
      Automation graphs — deterministic create from explicit `nodes` +
      `connections`, update, version, publish / unpublish, test. Includes
      trigger event definitions + the fire endpoint (`/v1/automations/triggers`)
      and run history (`/v1/automations/runs`).
  - name: Contacts
    description: Create, search, patch, and delete contacts. Email is the primary key.
  - name: Contact Fields
    description: List, create, and delete custom contact field definitions.
  - name: Audiences
    description: Saved contact filter sets — a recipient target for sends.
  - name: Domains
    description: 'Sending domains: add, read DNS records, verify, configure sender defaults.'
  - name: Templates
    description: Public template gallery (read-only) usable as generation references.
  - name: Brand
    description: The single brand bound to the API key.
  - name: Chats
    description: >-
      List the brand's Brew chats, and read a brand-scoped digest of one —
      referenced emails/automations/triggers + a trimmed transcript — so an
      external agent can resume the conversation.
  - name: Notifications
    description: >-
      The brand's notification feed, as the app's bell shows it. Each row is
      gated by the credential's access to its feature, and a comment mention
      reaches only the person it names.
  - name: Insights
    description: >-
      Brew Insights, read-only: the insight engine’s deterministic findings
      about the brand, ranked as the Insights page shows them, one finding in
      full with its frozen metrics and evidence, and the intelligence layer
      beside them (weekly pulse, latest report, open suggestions, agent memo).
  - name: Integrations
    description: >-
      Brand-scoped catalog of connectable providers plus which ones are already
      connected. Connect itself stays in Settings (`/integrations/{provider}`).
  - name: API Keys
    description: >-
      Mint, list, and revoke API keys through a signed-in Clerk session whose
      active organization role is exactly `org:admin`. API-key and OAuth actors
      receive `403`. `POST` body `brandId` is the new key's binding (the only v1
      body field named `brandId`).
  - name: Meta
    description: >-
      Public discovery surface (no auth): the machine-readable API catalog
      (`/v1/help`).
paths:
  /v1/contacts/import:
    post:
      tags:
        - Contacts
      summary: Bulk-import contacts from JSON
      description: >-
        Create or update up to 1000 contacts from JSON rows and record them as
        one Import History batch: the JSON twin of `POST
        /v1/contacts/import-csv`. Rows take the batch-create shape (`email`,
        `firstName`, `lastName`, `subscribed`, `customFields`, `consent`) and
        follow its rules: a missing or invalid email, or an address repeated in
        the batch, fails its row (`errors[]`, `207`) while the others land. New
        contacts are subscribed unless the row says `subscribed: false`;
        re-subscribing a contact who opted out keeps its other fields and adds a
        `RESUBSCRIBE_SKIPPED` warning.


        The batch is listed in Import History under `fileName` (optional), and
        each contact it writes carries the returned `importId` until a later
        import touches it (filter on it to list them; deleting the import in the
        app removes the contacts it created). `POST /v1/contacts` with
        `contacts[]` writes the same rows without an Import History entry, for
        recurring syncs.


        Two opt-in modes. `dryRun: true` returns the response this import would
        return (`summary`, `errors`, row `warnings`, `fieldsCreated` as fields
        it would create) with `dryRun: true` and no `importId`, and writes
        nothing: no contacts, field definitions or Import History entry;
        `validate` is not run and nothing is charged. Insert vs update is
        decided by the same address match the write uses, so a real run right
        after returns the same counts. `onConflict: "skip"` only creates missing
        contacts: each existing contact is left unchanged (an opt-out row
        included), counted in `summary.skipped` with a `CONTACT_EXISTS_SKIPPED`
        warning naming its `row`. Either way `inserted + updated + failed +
        skipped = total`. A dry run cannot foresee failures that only happen
        while writing (a concurrent change, a database refusal), so the real run
        can still fail where the preview succeeded. With `validate: true`,
        inline checks are still metered per submitted row, rows `skip` left
        alone included; a request that leaves no address to check is not
        charged.
      operationId: importContacts
      parameters:
        - name: X-Brand-Id
          in: header
          required: false
          description: >-
            The brand this request acts on. REQUIRED for organization-scoped
            credentials (otherwise `400 BRAND_ID_REQUIRED` — there is no default
            brand); list ids with `GET /v1/brands`. Brand-scoped credentials may
            omit it, and sending a different brand returns `403
            BRAND_SCOPE_MISMATCH`. A brand outside your organization returns
            `404 BRAND_NOT_FOUND`.
          schema:
            type: string
            minLength: 1
            maxLength: 64
          example: kx7b3s7fapqz8mjm12ekz1kxdx87yceg
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactsImportRequest'
            example:
              contacts:
                - email: ada@example.com
                  firstName: Ada
                - email: grace@example.com
                  firstName: Grace
                  customFields:
                    plan: pro
              consent:
                source: import
              fileName: crm-export.json
      responses:
        '200':
          description: Import summary (all rows valid).
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
            X-RateLimit-Limit:
              schema:
                type: integer
                description: Requests allowed in the current rolling rate limit window.
                example: 100
              required: true
              description: Requests allowed in the current rolling rate limit window.
            X-RateLimit-Remaining:
              schema:
                type: integer
                description: Requests remaining in the current rolling rate limit window.
                example: 99
              required: true
              description: Requests remaining in the current rolling rate limit window.
            X-RateLimit-Reset:
              schema:
                type: integer
                description: >-
                  Unix timestamp in seconds when the oldest recorded request
                  leaves the rolling window.
                example: 1712592360
              required: true
              description: >-
                Unix timestamp in seconds when the oldest recorded request
                leaves the rolling window.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsImportResponse'
              example:
                summary:
                  inserted: 2
                  updated: 0
                  failed: 0
                  skipped: 0
                  total: 2
                fieldsCreated:
                  - plan
                errors: []
                warnings: []
                importId: imp_01JABCDEF0123456789XYZ
        '207':
          description: Partial success — some rows failed validation.
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
            X-RateLimit-Limit:
              schema:
                type: integer
                description: Requests allowed in the current rolling rate limit window.
                example: 100
              required: true
              description: Requests allowed in the current rolling rate limit window.
            X-RateLimit-Remaining:
              schema:
                type: integer
                description: Requests remaining in the current rolling rate limit window.
                example: 99
              required: true
              description: Requests remaining in the current rolling rate limit window.
            X-RateLimit-Reset:
              schema:
                type: integer
                description: >-
                  Unix timestamp in seconds when the oldest recorded request
                  leaves the rolling window.
                example: 1712592360
              required: true
              description: >-
                Unix timestamp in seconds when the oldest recorded request
                leaves the rolling window.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsImportResponse'
              example:
                summary:
                  inserted: 1
                  updated: 0
                  failed: 1
                  skipped: 0
                  total: 2
                fieldsCreated: []
                errors:
                  - email: not-an-email
                    row: 2
                    code: INVALID_EMAIL
                    message: '''not-an-email'' is not a valid email address.'
                warnings: []
                importId: imp_01JABCDEF0123456789XYZ
        '400':
          description: >-
            `BRAND_ID_REQUIRED`: An organization-scoped credential called a
            brand-scoped operation without naming the brand.


            `INVALID_REQUEST`: The body or query failed validation: an unknown
            key, a wrong type, or a missing required field. `param` names the
            offender.
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorEnvelope'
              examples:
                brandIdRequired:
                  summary: BRAND_ID_REQUIRED
                  value:
                    error:
                      code: BRAND_ID_REQUIRED
                      type: invalid_request
                      message: >-
                        An organization-scoped credential called a brand-scoped
                        operation without naming the brand.
                      suggestion: >-
                        List brands with GET /v1/brands, then name one with the
                        X-Brand-Id header. There is no default brand.
                      docs: https://docs.brew.new/api-reference/api/authentication
                invalidRequest:
                  summary: INVALID_REQUEST
                  value:
                    error:
                      code: INVALID_REQUEST
                      type: invalid_request
                      message: >-
                        The body or query failed validation: an unknown key, a
                        wrong type, or a missing required field. `param` names
                        the offender.
                      suggestion: Fix the field reported in `param` and retry.
                      docs: https://docs.brew.new/api-reference/api/errors
        '401':
          description: >-
            `API_KEY_REVOKED`: The API key was revoked.


            `AUTHENTICATION_REQUIRED`: No API key or session accompanied the
            request.


            `INVALID_API_KEY`: The API key is malformed or unknown.
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorEnvelope'
              examples:
                apiKeyRevoked:
                  summary: API_KEY_REVOKED
                  value:
                    error:
                      code: API_KEY_REVOKED
                      type: authentication_error
                      message: The API key was revoked.
                      suggestion: Create a new active API key and retry.
                      docs: https://docs.brew.new/api-reference/api/authentication
                authenticationRequired:
                  summary: AUTHENTICATION_REQUIRED
                  value:
                    error:
                      code: AUTHENTICATION_REQUIRED
                      type: authentication_error
                      message: No API key or session accompanied the request.
                      suggestion: >-
                        Provide a valid API key or sign in with an organization
                        session.
                      docs: https://docs.brew.new/api-reference/api/authentication
                invalidApiKey:
                  summary: INVALID_API_KEY
                  value:
                    error:
                      code: INVALID_API_KEY
                      type: authentication_error
                      message: The API key is malformed or unknown.
                      suggestion: >-
                        Check the API key format and retry with a valid active
                        key.
                      docs: https://docs.brew.new/api-reference/api/authentication
        '402':
          description: >-
            `INSUFFICIENT_CREDITS`: The credit balance is below what this
            operation requires; `details.cost` is the amount needed.
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorEnvelope'
              examples:
                insufficientCredits:
                  summary: INSUFFICIENT_CREDITS
                  value:
                    error:
                      code: INSUFFICIENT_CREDITS
                      type: payment_required
                      message: >-
                        The credit balance is below what this operation
                        requires; `details.cost` is the amount needed.
                      suggestion: >-
                        Upgrade your plan or wait for the next billing period to
                        reset. Check your balance up front with GET /v1/usage.
                      docs: https://docs.brew.new/api-reference/api/credits
                      details:
                        cost: 5
                        remaining: 0
                        planKey: free
        '403':
          description: >-
            `ACCOUNT_SUSPENDED`: The organization behind the credential is
            suspended.


            `BRAND_SCOPE_MISMATCH`: A brand-scoped credential named a brand
            other than the one it is bound to.


            `INSUFFICIENT_PERMISSIONS`: The credential lacks the permission
            scope the operation needs.


            `INSUFFICIENT_ROLE`: The caller lacks the access the operation
            needs: `param` names `member` (access to the brand) or `org_admin`
            (the organization role).
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorEnvelope'
              examples:
                accountSuspended:
                  summary: ACCOUNT_SUSPENDED
                  value:
                    error:
                      code: ACCOUNT_SUSPENDED
                      type: authorization_error
                      message: The organization behind the credential is suspended.
                      suggestion: >-
                        The organization behind this credential is suspended.
                        Contact support@brew.new; do not retry.
                      docs: https://docs.brew.new/api-reference/api/authentication
                brandScopeMismatch:
                  summary: BRAND_SCOPE_MISMATCH
                  value:
                    error:
                      code: BRAND_SCOPE_MISMATCH
                      type: authorization_error
                      message: >-
                        A brand-scoped credential named a brand other than the
                        one it is bound to.
                      suggestion: >-
                        Omit the brand to use the one this credential is bound
                        to, or use an organization-scoped credential to reach
                        other brands.
                      docs: https://docs.brew.new/api-reference/api/authentication
                insufficientPermissions:
                  summary: INSUFFICIENT_PERMISSIONS
                  value:
                    error:
                      code: INSUFFICIENT_PERMISSIONS
                      type: authorization_error
                      message: >-
                        The credential lacks the permission scope the operation
                        needs.
                      suggestion: Use an API key or session with the required permission.
                      docs: https://docs.brew.new/api-reference/api/authentication
                insufficientRole:
                  summary: INSUFFICIENT_ROLE
                  value:
                    error:
                      code: INSUFFICIENT_ROLE
                      type: authorization_error
                      message: >-
                        The caller lacks the access the operation needs: `param`
                        names `member` (access to the brand) or `org_admin` (the
                        organization role).
                      suggestion: >-
                        Ask an organization admin to run this, to add you to the
                        brand, or to make you an admin.
                      docs: https://docs.brew.new/api-reference/api/authentication
        '404':
          description: >-
            `BRAND_NOT_FOUND`: The named or bound brand does not exist in this
            organization (unknown, deleting, or another organization).
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorEnvelope'
              examples:
                brandNotFound:
                  summary: BRAND_NOT_FOUND
                  value:
                    error:
                      code: BRAND_NOT_FOUND
                      type: not_found
                      message: >-
                        The named or bound brand does not exist in this
                        organization (unknown, deleting, or another
                        organization).
                      suggestion: >-
                        List the brands this credential can reach with GET
                        /v1/brands.
                      docs: https://docs.brew.new/api-reference/api/errors
        '409':
          description: >-
            `IDEMPOTENCY_CONFLICT`: The same Idempotency-Key was reused with a
            different request body.


            `IDEMPOTENCY_IN_PROGRESS`: A request with this Idempotency-Key is
            still executing.
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorEnvelope'
              examples:
                idempotencyConflict:
                  summary: IDEMPOTENCY_CONFLICT
                  value:
                    error:
                      code: IDEMPOTENCY_CONFLICT
                      type: conflict
                      message: >-
                        The same Idempotency-Key was reused with a different
                        request body.
                      suggestion: >-
                        Reuse the original payload or send a new idempotency
                        key.
                      docs: https://docs.brew.new/api-reference/api/idempotency
                idempotencyInProgress:
                  summary: IDEMPOTENCY_IN_PROGRESS
                  value:
                    error:
                      code: IDEMPOTENCY_IN_PROGRESS
                      type: conflict
                      message: A request with this Idempotency-Key is still executing.
                      suggestion: >-
                        Wait for the original request to finish, then retry the
                        same key to replay its response.
                      docs: https://docs.brew.new/api-reference/api/idempotency
        '429':
          description: >-
            `RATE_LIMITED`: The credential exhausted the rolling window for this
            route policy; Retry-After is when the oldest recorded request
            leaves, and the refused request does not take a slot.
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
            X-RateLimit-Limit:
              schema:
                type: integer
                description: Requests allowed in the current rolling rate limit window.
                example: 10
              required: true
              description: Requests allowed in the current rolling rate limit window.
            X-RateLimit-Remaining:
              schema:
                type: integer
                description: Requests remaining in the current rolling rate limit window.
                example: 0
              required: true
              description: Requests remaining in the current rolling rate limit window.
            X-RateLimit-Reset:
              schema:
                type: integer
                description: >-
                  Unix timestamp in seconds when the oldest recorded request
                  leaves the rolling window.
                example: 1712592360
              required: true
              description: >-
                Unix timestamp in seconds when the oldest recorded request
                leaves the rolling window.
            Retry-After:
              schema:
                type: integer
                description: Seconds to wait before retrying the request.
                example: 42
              required: true
              description: Seconds to wait before retrying the request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorEnvelope'
              examples:
                rateLimited:
                  summary: RATE_LIMITED
                  value:
                    error:
                      code: RATE_LIMITED
                      type: rate_limit
                      message: >-
                        The credential exhausted the rolling window for this
                        route policy; Retry-After is when the oldest recorded
                        request leaves, and the refused request does not take a
                        slot.
                      suggestion: Wait the Retry-After time, then retry the same request.
                      docs: https://docs.brew.new/api-reference/api/rate-limits
                      retryAfter: 42
                      details:
                        scope: credential
                        limit: 10
                        remaining: 0
                        reset: 1712592360
                        consumed: false
        '500':
          description: >-
            `INTERNAL_ERROR`: An unexpected failure; the x-request-id header
            identifies it.
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorEnvelope'
              examples:
                internalError:
                  summary: INTERNAL_ERROR
                  value:
                    error:
                      code: INTERNAL_ERROR
                      type: internal_error
                      message: >-
                        An unexpected failure; the x-request-id header
                        identifies it.
                      suggestion: >-
                        Retry the request. If it keeps failing, contact support
                        with the x-request-id header.
                      docs: https://docs.brew.new/api-reference/api/errors
        '503':
          description: >-
            `SERVICE_UNAVAILABLE`: A dependency the operation must consult
            (billing, the idempotency store) is temporarily unavailable, so the
            request was refused rather than run unmetered.
          headers:
            x-request-id:
              schema:
                type: string
                description: >-
                  Unique request identifier. Share this with support when
                  debugging a request.
                example: req_8cac13fd94e6420cacdd75a1aa403a28
              required: true
              description: >-
                Unique request identifier. Share this with support when
                debugging a request.
            Retry-After:
              schema:
                type: integer
                description: Seconds to wait before retrying the request.
                example: 42
              required: true
              description: Seconds to wait before retrying the request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorEnvelope'
              examples:
                serviceUnavailable:
                  summary: SERVICE_UNAVAILABLE
                  value:
                    error:
                      code: SERVICE_UNAVAILABLE
                      type: service_unavailable
                      message: >-
                        A dependency the operation must consult (billing, the
                        idempotency store) is temporarily unavailable, so the
                        request was refused rather than run unmetered.
                      suggestion: Retry the request after a short delay.
                      docs: https://docs.brew.new/api-reference/api/errors
components:
  schemas:
    ContactsImportRequest:
      type: object
      properties:
        contacts:
          type: array
          items:
            type: object
            properties:
              email:
                type: string
              firstName:
                type: string
              lastName:
                type: string
              subscribed:
                type: boolean
              customFields:
                type: object
                additionalProperties: {}
              consent:
                type: object
                properties:
                  source:
                    type: string
                    enum:
                      - api
                      - form
                      - import
                    description: >-
                      Who captured it: `api`, `form`, or `import`. (`app` is
                      reserved for the in-app CSV wizard.)
                  capturedAt:
                    type: string
                    format: date-time
                    description: ISO 8601; defaults to now.
                  policyVersion:
                    type: string
                    minLength: 1
                    maxLength: 64
                  evidence:
                    type: string
                    minLength: 1
                    maxLength: 500
                required:
                  - source
                additionalProperties: false
                description: >-
                  Marketing consent provenance to record on this contact.
                  Optional at creation; an inline marketing send to this address
                  later needs the contact to be subscribed and warns when no
                  record exists.
            additionalProperties: false
          minItems: 1
          maxItems: 1000
        validate:
          type: boolean
          description: >-
            Optional deliverability check on ingestion. When true, each imported
            address is validated with the provider and the verdict is saved to
            the contact’s `validationStatus`. Imports of at most 100 submitted
            contacts validate inline, metered at 2 credits per submitted contact
            on a successful response, including rows that fail. Larger imports
            write first and use a background validation workflow billed per
            address as it runs; `validationJobId` is returned when a job is
            queued.
        consent:
          type: object
          properties:
            source:
              type: string
              enum:
                - api
                - form
                - import
              description: >-
                Who captured it: `api`, `form`, or `import`. (`app` is reserved
                for the in-app CSV wizard.)
            capturedAt:
              type: string
              format: date-time
              description: ISO 8601; defaults to now.
            policyVersion:
              type: string
              minLength: 1
              maxLength: 64
            evidence:
              type: string
              minLength: 1
              maxLength: 500
          required:
            - source
          additionalProperties: false
          description: >-
            Default consent record for every row that does not carry its own
            `consent` (typically `{ "source": "import" }`).
        fileName:
          type: string
          minLength: 1
          maxLength: 255
          description: >-
            Name this batch is listed under in Import History (usually the
            source file name, e.g. `customers.csv`), also stamped on every
            contact it writes as `csvFileName`. A path keeps only its last
            segment.
        dryRun:
          type: boolean
          description: >-
            When true, returns what this import would return and writes nothing:
            no contacts, fields, Import History entry or `importId`; `validate`
            is skipped and nothing is charged.
        onConflict:
          type: string
          enum:
            - upsert
            - skip
          description: >-
            `upsert` (default) updates existing contacts; `skip` only creates
            missing ones, leaving each existing contact unchanged (counted in
            `summary.skipped`, with a `CONTACT_EXISTS_SKIPPED` warning).
      required:
        - contacts
      additionalProperties: false
    ContactsImportResponse:
      type: object
      properties:
        summary:
          type: object
          properties:
            inserted:
              type: integer
              minimum: 0
            updated:
              type: integer
              minimum: 0
            failed:
              type: integer
              minimum: 0
            skipped:
              type: integer
              minimum: 0
              description: >-
                Rows not written and not failed: existing contacts left alone by
                `onConflict: "skip"` (one `CONTACT_EXISTS_SKIPPED` warning
                each), and CSV rows omitted for a missing/invalid email or
                merged into a later duplicate (up to 100 `CSV_ROW_SKIPPED`
                warnings name each `row`; a final warning counts further skipped
                rows).
            total:
              type: integer
              minimum: 0
              description: >-
                Rows submitted (JSON contacts, or CSV data rows). Always
                `inserted + updated + failed + skipped`.
          required:
            - inserted
            - updated
            - failed
            - skipped
            - total
          additionalProperties: false
        fieldsCreated:
          type: array
          items:
            type: string
        errors:
          type: array
          items:
            type: object
            properties:
              email:
                type: string
              row:
                type: integer
                exclusiveMinimum: 0
                description: >-
                  The 1-based row that failed: `contacts[row - 1]`, or the CSV
                  data row (header and blank lines not counted).
              code:
                type: string
              field:
                type: string
              message:
                type: string
            required:
              - email
              - code
              - message
            additionalProperties: false
        warnings:
          type: array
          items:
            type: object
            properties:
              code:
                type: string
                minLength: 1
              email:
                type: string
                minLength: 1
              row:
                type: integer
                exclusiveMinimum: 0
                description: >-
                  The 1-based row it is about (CSV: data row; header and blank
                  lines not counted).
              field:
                type: string
                minLength: 1
              message:
                type: string
                minLength: 1
              from:
                type: string
                minLength: 1
              to:
                type: string
                minLength: 1
              original:
                type: string
                minLength: 1
              normalized:
                type: string
                minLength: 1
            required:
              - code
              - message
            additionalProperties: false
        validation:
          type: object
          properties:
            valid:
              type: integer
              minimum: 0
            risky:
              type: integer
              minimum: 0
            invalid:
              type: integer
              minimum: 0
            unscored:
              type: integer
              minimum: 0
            persistence:
              type: object
              properties:
                matched:
                  type: integer
                  minimum: 0
                modified:
                  type: integer
                  minimum: 0
                failed:
                  type: integer
                  minimum: 0
              required:
                - matched
                - modified
                - failed
              additionalProperties: false
          required:
            - valid
            - risky
            - invalid
            - unscored
          additionalProperties: false
        validationJobId:
          type: string
        importId:
          type: string
          description: >-
            This batch's Import History id. Each contact it wrote carries it as
            `importId` until a later import touches that contact, so a contact
            filter on `importId` lists them. Absent on a dry run.
        dryRun:
          type: boolean
          enum:
            - true
          description: >-
            Present on a dry run: the counts say what the import would do, and
            nothing was written.
      required:
        - summary
        - fieldsCreated
        - errors
        - warnings
      additionalProperties: false
    ApiErrorEnvelope:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              $ref: '#/components/schemas/ApiErrorCode'
            type:
              type: string
              enum:
                - authentication_error
                - authorization_error
                - invalid_request
                - not_found
                - not_implemented
                - conflict
                - rate_limit
                - payment_required
                - service_unavailable
                - internal_error
            message:
              type: string
              minLength: 1
            param:
              type: string
              minLength: 1
            suggestion:
              type: string
              minLength: 1
            docs:
              type: string
              format: uri
            retryAfter:
              type: integer
              minimum: 0
            details:
              type: object
              additionalProperties: {}
          required:
            - code
            - type
            - message
            - suggestion
            - docs
      required:
        - error
    ApiErrorCode:
      type: string
      enum:
        - ACCOUNT_SUSPENDED
        - API_KEY_REVOKED
        - AUDIENCE_BUILD_ACTIVE
        - AUDIENCE_BUILD_ALREADY_ACTIVE
        - AUDIENCE_EDIT_CONFLICT
        - AUDIENCE_MEMBERSHIP_NOT_EXPRESSIBLE
        - AUDIENCE_NOT_FOUND
        - AUDIENCE_RUN_NOT_FOUND
        - AUDIT_NOT_FOUND
        - AUTHENTICATION_REQUIRED
        - AUTOMATION_GRAPH_INVALID
        - AUTOMATION_NOT_FOUND
        - AUTOMATION_NOT_PAUSABLE
        - AUTOMATION_NOT_PUBLISHED
        - AUTOMATION_RUN_NOT_FOUND
        - AUTOMATION_VERSION_CONFLICT
        - AUTOMATION_VERSION_NOT_FOUND
        - BATCH_TOO_LARGE
        - BRAND_DOMAIN_CONFLICT
        - BRAND_ID_REQUIRED
        - BRAND_LIMIT_REACHED
        - BRAND_NOT_FOUND
        - BRAND_NOT_READY
        - BRAND_SCOPE_MISMATCH
        - CHAT_NOT_FOUND
        - COMMENT_NOT_FOUND
        - CONSENT_REQUIRED
        - CONTACT_NOT_FOUND
        - CONTENT_OPERATION_FAILED
        - CONTRACT_LOCKED_BY_PUBLISHED_AUTOMATIONS
        - CORE_FIELD_IMMUTABLE
        - DOMAIN_ALREADY_EXISTS
        - DOMAIN_CLAIMED_ELSEWHERE
        - DOMAIN_NOT_FOUND
        - DOMAIN_NOT_READY
        - DOMAIN_OTHER_BRAND
        - DOMAIN_PROVIDER_ERROR
        - DOMAIN_PURPOSE_NOT_ALLOWED
        - DOMAIN_VERIFICATION_FAILED
        - DOMAIN_VERIFIED_ELSEWHERE
        - EMAIL_GENERATION_FAILED
        - EMAIL_GROUP_NAME_CONFLICT
        - EMAIL_GROUP_NOT_FOUND
        - EMAIL_IMAGES_MISSING
        - EMAIL_IMPORT_FAILED
        - EMAIL_IN_PROGRESS
        - EMAIL_IN_USE_BY_AUTOMATION
        - EMAIL_NOT_FOUND
        - EMAIL_NOT_READY
        - EMAIL_RESULT_UNAVAILABLE
        - EMAIL_RUN_AMBIGUOUS
        - EMAIL_TEMPLATE_INVALID
        - EMAIL_VERSION_NOT_FOUND
        - EXPORT_PROVIDER_ERROR
        - EXPORT_UNSUPPORTED
        - FIELD_NOT_FOUND
        - FIELD_TYPE_MISMATCH
        - FIGMA_ACCESS_DENIED
        - FIGMA_CONVERSION_FAILED
        - FIGMA_FRAME_NOT_FOUND
        - FIGMA_NOT_CONNECTED
        - FIGMA_UNAVAILABLE
        - FIGMA_URL_INVALID
        - FLOW_NOT_FOUND
        - IDEMPOTENCY_CONFLICT
        - IDEMPOTENCY_IN_PROGRESS
        - INSIGHT_NOT_FOUND
        - INSUFFICIENT_CREDITS
        - INSUFFICIENT_PERMISSIONS
        - INSUFFICIENT_ROLE
        - INTEGRATION_NOT_CONNECTED
        - INTERNAL_ERROR
        - INVALID_API_KEY
        - INVALID_EMAIL
        - INVALID_PAYLOAD
        - INVALID_REQUEST
        - LIQUID_RENDER_ERROR
        - METHOD_NOT_ALLOWED
        - MISSING_EMAIL
        - NO_ELIGIBLE_RECIPIENTS
        - NO_PUBLISHED_AUTOMATION
        - NOT_FOUND
        - NOT_IMPLEMENTED
        - ORG_SCOPE_REQUIRED
        - PAYLOAD_SCHEMA_EMAIL_REQUIRED
        - PAYLOAD_TOO_LARGE
        - PREVIEW_NOT_FOUND
        - PUBLISH_VALIDATION_FAILED
        - RATE_LIMITED
        - RECIPIENT_UNSUBSCRIBED
        - REFERENCE_EMAIL_NOT_FOUND
        - RESUBSCRIBE_NOT_ALLOWED
        - RUN_IN_PROGRESS
        - RUN_NOT_CANCELLABLE
        - RUN_NOT_PAUSABLE
        - RUN_NOT_PAUSED
        - RUN_NOT_RESUMABLE
        - RUN_START_FAILED
        - RUN_STOP_FAILED
        - SEND_NOT_CANCELLABLE
        - SEND_NOT_FOUND
        - SEND_NOT_PAUSABLE
        - SEND_NOT_RESUMABLE
        - SEND_QUOTA_EXCEEDED
        - SERVICE_UNAVAILABLE
        - TEMPLATE_NOT_FOUND
        - TRIGGER_ALREADY_EXISTS
        - TRIGGER_EVENT_NOT_FOUND
        - TRIGGER_HAS_DEPENDENT_AUTOMATIONS
        - TRIGGER_IMMUTABLE
        - TRIGGER_INSTANCE_NOT_FOUND
        - TRIGGER_LIMIT_REACHED
        - TRIGGER_NOT_FIREABLE
        - UPLOAD_IN_PROGRESS
        - UPLOAD_NOT_FOUND
        - UPLOAD_NOT_RECEIVED
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: 'Send your Brew API key as `Authorization: Bearer brew_xxx`.'
      x-default: Bearer brew_your_api_key
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: 'Send your Brew API key as `X-API-Key: brew_xxx`.'
      x-default: brew_your_api_key

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.