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

# Bulk send AR statements

> <Note>Required API key scope: `accounting:read`</Note>

Operator-triggered bulk delivery of AR statements to many customer accounts at once.
Sends one statement email per eligible customer covering the requested period
(default: previous calendar month). Mirrors the same per-account loop the monthly
statement cron runs, with a richer report (batch summary + skipped reasons +
failed ids) and explicit `account_ids` / `filter` inputs.

**Industry parity:** NetSuite "Email Statements" bulk action, QuickBooks Online
"Create Statements" multi-select send, Acumatica AR Customer Statements form.

**Per-account gates (in order):** (1) account must have an email on file, (2) email
must not be a smoke / test domain, (3) 4-layer customer-email gate (entity disable /
per-account opt-out / global unsubscribe / template unsubscribe; statement_email
defaults to OFF entity-wide so the entity must explicitly opt in), (4) idempotency
within a 30-day rolling window per (entity, recipient, statement_email).

**Throttle:** processed in chunks of 25 with a 50ms yield between chunks to respect
the Postmark 10/sec rate limit. Same chunk size as the cron path.

**Dry-run:** pass `dry_run: true` to return the eligible recipient set without sending.
Used by the UI confirm modal to show batch count + per-account skip reasons up front.




## OpenAPI

````yaml /openapi.yaml post /accounting/statements/bulk-send
openapi: 3.1.0
info:
  title: Arcus ERP Public API
  version: 1.0.0
  description: >
    Arcus ERP public REST API. Designed for external integrations and data
    migration.


    **Authentication.** Bearer token (API key) via the `Authorization` header.

    Format: `Authorization: Bearer ark_live_ent_<code>_<random>` (or
    `ark_test_*` for sandbox).

    API keys are issued per-entity in **Settings > Developers > API Keys**.


    **Entity scoping.** The entity is encoded in the API key prefix; routes are
    flat

    (e.g. `/v1/accounts`, `/v1/orders`, `/v1/products`). A small set of platform
    endpoints

    (migration, reconciliation, events, webhook endpoints, API keys) use the

    `/v1/entities/{entity_id}/...` form -- those are noted in their tags.


    **Key capabilities.**
      - Related-resource hydration via `?expand[]=` (see `x-arcus-expand` on each resource).
      - Cursor-based pagination (`starting_after` / `ending_before` / `limit`).
      - Idempotency via the `Idempotency-Key` header.
      - Webhook events for asynchronous notification.
      - Conditional requests / ETag for cache validation.
servers:
  - url: https://api.arcuserp.com/v1
    description: Arcus ERP API (accepts both live `ark_live_*` and test `ark_test_*` keys)
  - url: https://dev-api.arcuserp.com/v1
    description: >-
      Dev sandbox API (test-only data, accepts `ark_test_*` keys against dev
      RDS)
security: []
paths:
  /accounting/statements/bulk-send:
    post:
      tags:
        - Accounting
      summary: Bulk send AR statements
      description: >
        <Note>Required API key scope: `accounting:read`</Note>


        Operator-triggered bulk delivery of AR statements to many customer
        accounts at once.

        Sends one statement email per eligible customer covering the requested
        period

        (default: previous calendar month). Mirrors the same per-account loop
        the monthly

        statement cron runs, with a richer report (batch summary + skipped
        reasons +

        failed ids) and explicit `account_ids` / `filter` inputs.


        **Industry parity:** NetSuite "Email Statements" bulk action, QuickBooks
        Online

        "Create Statements" multi-select send, Acumatica AR Customer Statements
        form.


        **Per-account gates (in order):** (1) account must have an email on
        file, (2) email

        must not be a smoke / test domain, (3) 4-layer customer-email gate
        (entity disable /

        per-account opt-out / global unsubscribe / template unsubscribe;
        statement_email

        defaults to OFF entity-wide so the entity must explicitly opt in), (4)
        idempotency

        within a 30-day rolling window per (entity, recipient, statement_email).


        **Throttle:** processed in chunks of 25 with a 50ms yield between chunks
        to respect

        the Postmark 10/sec rate limit. Same chunk size as the cron path.


        **Dry-run:** pass `dry_run: true` to return the eligible recipient set
        without sending.

        Used by the UI confirm modal to show batch count + per-account skip
        reasons up front.
      operationId: bulkSendStatementsV1
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                account_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: >-
                    Explicit list of customer account UUIDs to send to. When
                    set, overrides `filter`.
                filter:
                  type: object
                  description: >-
                    Filter-driven eligibility when account_ids is omitted.
                    Mirrors the AR Aging filter set.
                  properties:
                    has_open_balance:
                      type: boolean
                    overdue_only:
                      type: boolean
                    search:
                      type: string
                from:
                  type: string
                  format: date
                  description: >-
                    Statement period start (ISO date). Defaults to first day of
                    previous calendar month.
                to:
                  type: string
                  format: date
                  description: >-
                    Statement period end (ISO date). Defaults to last day of
                    previous calendar month.
                dry_run:
                  type: boolean
                  default: false
                  description: >-
                    Preview mode: returns eligible recipients with skip reasons
                    without sending any email.
      responses:
        '200':
          description: Batch send results
          content:
            application/json:
              schema:
                type: object
                properties:
                  batch_id:
                    type: string
                    format: uuid
                  dry_run:
                    type: boolean
                  period:
                    type: object
                    properties:
                      from:
                        type: string
                        format: date
                      to:
                        type: string
                        format: date
                  summary:
                    type: object
                    properties:
                      eligible:
                        type: integer
                        description: Total accounts evaluated
                      sent:
                        type: integer
                        description: Accounts that received a statement email
                      skipped:
                        type: integer
                        description: >-
                          Accounts skipped (no_email / fake_domain /
                          entity_disabled / already_sent_this_period /
                          gate_blocked / etc)
                      failed:
                        type: integer
                        description: Accounts that hit a send error
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        account_id:
                          type: string
                          format: uuid
                        customer_name:
                          type: string
                        email:
                          type: string
                          nullable: true
                        status:
                          type: string
                          enum:
                            - sent
                            - skipped
                            - failed
                            - would_send
                        reason:
                          type: string
                          nullable: true
                          description: Populated when status=skipped
                        postmark_message_id:
                          type: string
                          nullable: true
                          description: Populated when status=sent
                        error_message:
                          type: string
                          nullable: true
                          description: Populated when status=failed
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
      security:
        - ApiKeyAuth: []
components:
  schemas:
    Error:
      description: |
        Alias for ErrorEnvelope. Canonical error response shape used by all
        API endpoints. Refer to ErrorEnvelope for the full field definition.
      allOf:
        - $ref: '#/components/schemas/ErrorEnvelope'
    ErrorEnvelope:
      type: object
      description: |
        Canonical error response envelope. All API errors use this shape.
      required:
        - error
        - code
      properties:
        error:
          type: string
          description: Machine-readable error key
          example: not_found
        code:
          type: string
          description: Machine-readable error code (often same as error)
          example: not_found
        type:
          type: string
          enum:
            - validation_error
            - permission_error
            - not_found
            - conflict
            - rate_limit
            - internal
            - expand_error
            - not_implemented
          example: not_found
        hint:
          type: string
          description: Human-readable one-sentence explanation (English)
          example: >-
            The requested order does not exist or does not belong to this
            entity.
        param:
          type: string
          description: The parameter that caused the error, if applicable
          example: expand[0]
        required:
          type: string
          description: The scope required (only on insufficient_scope errors)
          example: accounts:read
        request_id:
          type: string
          description: >-
            Unique request ID for support tracing (maps to CloudWatch log
            stream)
          example: req_abc123
  responses:
    Error:
      description: Error response (400/401/403/404/409/422/429/500)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: |
        API key issued per entity via Settings > Developers > API Keys.
        Each key carries scopes (e.g. orders:read, products:write).
        Bearer token format: Authorization: Bearer ark_live_ent_<code>_<random>
        Test keys use ark_test_ent_<code>_<random>. Both are issued per entity
        via Settings > Developers > API Keys.

````