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

# Preview a campaign's recipients without sending (campagne, relance)

> Resolves the campaign's audience from its saved segment, filters it by per-channel consent (`marketing_email` for email, `marketing_sms` for SMS) and a usable contact, and returns the resulting plan — WITHOUT sending anything and without mutating the campaign. ALWAYS run this before `sendCampaign`: `audienceCount` is who the segment matched, `eligibleCount` who would actually be addressed, and the two `excluded*` counters say why the rest dropped (no consent vs no contact address). A tiny `eligibleCount` usually means the consent register, not the segment, is the problem. Note the response carries every eligible recipient's raw contact (email address or phone number), so only call it when you actually need the recipient list. 404s for an unknown campaign.



## OpenAPI

````yaml /openapi.json post /v1/campaigns/{id}/dry-run
openapi: 3.0.3
info:
  title: Solya POS API
  version: 1.0.0
  description: >-
    The Solya POS backend HTTP surface. Every documented operation is
    agent-ready: it carries an `operationId`, an agent-facing `description`, the
    `pos.*` scopes it enforces (`x-required-permissions`) and an `x-agent-tier`.
    Success responses return the payload as raw JSON; failures return the
    `ErrorResponse` envelope (`{ error: { code, message, statusCode } }`).
servers:
  - url: /
    description: The backend, relative to its deployed origin.
security: []
paths:
  /v1/campaigns/{id}/dry-run:
    post:
      tags:
        - Marketing
      summary: Preview a campaign's recipients without sending (campagne, relance)
      description: >-
        Resolves the campaign's audience from its saved segment, filters it by
        per-channel consent (`marketing_email` for email, `marketing_sms` for
        SMS) and a usable contact, and returns the resulting plan — WITHOUT
        sending anything and without mutating the campaign. ALWAYS run this
        before `sendCampaign`: `audienceCount` is who the segment matched,
        `eligibleCount` who would actually be addressed, and the two `excluded*`
        counters say why the rest dropped (no consent vs no contact address). A
        tiny `eligibleCount` usually means the consent register, not the
        segment, is the problem. Note the response carries every eligible
        recipient's raw contact (email address or phone number), so only call it
        when you actually need the recipient list. 404s for an unknown campaign.
      operationId: dryRunCampaign
      parameters:
        - schema:
            type: string
            minLength: 1
          in: path
          name: id
          required: true
          description: The server-minted campaign id (`cmp-<uuid>`) to act on.
      responses:
        '200':
          description: The consent-filtered recipient plan and the exclusion breakdown.
          content:
            application/json:
              schema:
                type: object
                properties:
                  channel:
                    type: string
                    enum:
                      - email
                      - sms
                  audienceCount:
                    type: integer
                    minimum: 0
                    maximum: 9007199254740991
                  eligibleCount:
                    type: integer
                    minimum: 0
                    maximum: 9007199254740991
                  excludedNoConsentCount:
                    type: integer
                    minimum: 0
                    maximum: 9007199254740991
                  excludedNoContactCount:
                    type: integer
                    minimum: 0
                    maximum: 9007199254740991
                  recipients:
                    type: array
                    items:
                      type: object
                      properties:
                        customerId:
                          type: string
                          minLength: 1
                        contact:
                          type: string
                          minLength: 1
                      required:
                        - customerId
                        - contact
                      additionalProperties: false
                required:
                  - channel
                  - audienceCount
                  - eligibleCount
                  - excludedNoConsentCount
                  - excludedNoContactCount
                  - recipients
                additionalProperties: false
                description: >-
                  The consent-filtered recipient plan and the exclusion
                  breakdown.
                example:
                  channel: email
                  audienceCount: 42
                  eligibleCount: 31
                  excludedNoConsentCount: 8
                  excludedNoContactCount: 3
                  recipients:
                    - customerId: cust-1
                      contact: camille.renard@example.com
        '400':
          description: >-
            The request failed schema validation; `error.fieldErrors` lists the
            fields.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorResponse'
        '401':
          description: No valid credential was presented — send a bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The actor is authenticated but lacks the required `pos.*` scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No resource matches the addressed identifier.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: An unexpected server error — safe to retry idempotent requests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearerAuth: []
components:
  schemas:
    ValidationErrorResponse:
      type: object
      required:
        - error
      additionalProperties: false
      description: >-
        A `VALIDATION_FAILED` envelope carrying the offending fields in
        `fieldErrors`.
      properties:
        error:
          type: object
          required:
            - code
            - message
            - statusCode
          additionalProperties: false
          properties:
            code:
              type: string
              enum:
                - VALIDATION_FAILED
            message:
              type: string
            statusCode:
              type: integer
            fieldErrors:
              type: array
              description: >-
                One entry per rejected field: the field path and why it was
                rejected.
              items:
                type: object
                required:
                  - field
                  - message
                additionalProperties: false
                properties:
                  field:
                    type: string
                    description: Dot-path of the offending field.
                  message:
                    type: string
                    description: Why the field was rejected.
    ErrorResponse:
      type: object
      required:
        - error
      additionalProperties: false
      description: The uniform failure envelope every non-2xx response returns.
      properties:
        error:
          type: object
          required:
            - code
            - message
            - statusCode
          additionalProperties: false
          properties:
            code:
              type: string
              enum:
                - VALIDATION_FAILED
                - UNAUTHORIZED
                - FORBIDDEN
                - NOT_FOUND
                - CONFLICT
                - BUSINESS_RULE_VIOLATION
                - INTERNAL_ERROR
              description: >-
                Machine-readable kernel `ResultCode` — branch on this, not on
                `message`.
            message:
              type: string
              description: >-
                Human-readable explanation. Safe to surface; never leaks server
                internals.
            statusCode:
              type: integer
              description: >-
                The HTTP status, mirrored into the body so a client need not
                read headers.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        `Authorization: Bearer <token>`. Accepts EITHER a Keycloak access token
        (scopes-in-token) OR an opaque POS session token; both resolve to the
        same `pos.*` scope vocabulary the route guards enforce.

````