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

# Attach Contacts

> Attaches existing contacts to a run. Idempotent - an id already on the run is skipped, so a repeated or overlapping batch attaches nothing twice. Allowed in every status except CANCELLED. On an already-launched run the new contacts are dispatched immediately and a COMPLETED run reopens to RUNNING; on a paused run nothing is dialled until you resume, and on a scheduled run they wait and go out with the rest at scheduledAt. More than the per-request cap is a 400 rather than a silent truncation - chunk it, which is safe.

Attaches existing contacts to a run.

## Overview

Contacts arrive here rather than at [create](/api-reference/runs/create-run), and this endpoint is **idempotent**: an id already on the run is skipped, not attached twice. That is what makes retries and overlapping chunks safe.

```javascript theme={null}
POST /runs/789e0123-.../contacts
{ "contactIds": ["contact-a", "contact-b"] }

→ { "attached": 2, "skipped": 0, "totalContacts": 2, "queued": 0 }
```

Send the same body again and you get `{ "attached": 0, "skipped": 2, "totalContacts": 2 }`. The counts report rows **actually changed**, so they tell you what your request did rather than what it asked for.

## Attaching to a run that is already going

This is the point of the endpoint, not an edge case. A run is a rolling sequence, not a frozen batch:

| Run status  | What happens                                                                        |
| ----------- | ----------------------------------------------------------------------------------- |
| `DRAFT`     | Contacts are added. Nothing is dialled until you launch.                            |
| `SCHEDULED` | Contacts are added. Nothing is dialled; they go out with the rest at `scheduledAt`. |
| `RUNNING`   | Contacts are added **and dispatched immediately** — `queued` says how many.         |
| `PAUSED`    | Contacts are added. Nothing is dialled; they go out when you resume.                |
| `COMPLETED` | Contacts are added, and the run **reopens to `RUNNING`**.                           |
| `CANCELLED` | `422`. A cancelled run is deliberately dead.                                        |

<Warning>
  Because a `COMPLETED` run reopens, do not treat `COMPLETED` as "this run is finished forever"
  if anything in your system still attaches to it.
</Warning>

Attaching into a `RUNNING` run spends money, so it runs the same all-or-nothing billing admission a launch does. Without that, attach would simply be the way around the gate: launch one contact, attach 999.

## Batch bounds

Up to **500 contact ids per request**. More is a `400`, not a silent truncation — a caller told "attached 500" out of 900 has no way to know which 400 were dropped.

Chunking is yours to do and safe to do, precisely because attaching an id twice is a no-op:

```javascript theme={null}
for (const chunk of chunks(contactIds, 500)) {
  await attach(runId, chunk); // overlapping or repeated chunks cost nothing
}
```

A run's **total** contact count is deliberately unbounded. Only the per-request batch is capped.

## Contacts must already exist

This endpoint attaches existing contacts; it does not create them. Create them first with [Create Contact](/api-reference/contacts/create-contact). An id that is not an active contact in your company is a `422` naming the ids it could not find, and nothing is attached.

## Error Scenarios

* **400 Bad Request**: more than 500 ids, an empty list, or a malformed id
* **402 Payment Required**: attaching into a live run the company cannot afford. Nothing is attached and nothing is dispatched
* **404 Not Found**: the run does not exist, or belongs to another company
* **422 Unprocessable Entity**: the run is cancelled, or an id is not a contact in this company
* **403 Forbidden**: the API key does not hold `write:runs` and `read:contacts`

## Related Resources

<CardGroup cols={2}>
  <Card title="Detach Contact" icon="user-minus" href="/api-reference/runs/detach-run-contact">
    Remove a contact from a draft
  </Card>

  <Card title="Launch Run" icon="play" href="/api-reference/runs/launch-run">
    Fan the draft out into conversations
  </Card>

  <Card title="Create Contact" icon="user-plus" href="/api-reference/contacts/create-contact">
    Create the people first
  </Card>

  <Card title="Runs Resource Guide" icon="layer-group" href="/guides/resources/runs">
    How a run batches conversations
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /runs/{id}/contacts
openapi: 3.0.0
info:
  title: InstaView API
  description: |-
    InstaView API Documentation

    ## Authentication

    All endpoints require API key authentication using Bearer token:
    ```
    Authorization: Bearer sk_your_api_key_here
    ```

    ## API Key Management

    The API Key module provides comprehensive key management for:
    - **Direct Client Keys**: Company-scoped keys for your applications
    - **ATS Partner Keys**: Resource-scoped keys for ATS integrations

    ### Key Features
    - HMAC-SHA256 hashing for API key storage
    - Configurable rate limiting
    - Comprehensive audit logging
    - Company-level isolation
    - Resource scoping for ATS partners
  version: 1.0.0
  contact: {}
servers:
  - url: https://api.instaview.sk
    description: Production API Gateway
security: []
tags: []
paths:
  /runs/{id}/contacts:
    post:
      tags:
        - Runs
      summary: Attach contacts to a run
      description: >-
        Attaches existing contacts to a run. Idempotent - an id already on the
        run is skipped, so a repeated or overlapping batch attaches nothing
        twice. Allowed in every status except CANCELLED. On an already-launched
        run the new contacts are dispatched immediately and a COMPLETED run
        reopens to RUNNING; on a paused run nothing is dialled until you resume,
        and on a scheduled run they wait and go out with the rest at
        scheduledAt. More than the per-request cap is a 400 rather than a silent
        truncation - chunk it, which is safe.
      operationId: PublicRunsController_attachContacts_v1
      parameters:
        - name: id
          required: true
          in: path
          schema:
            type: string
            format: uuid
        - name: companyId
          required: false
          in: query
          description: >-
            Required for ATS API keys to specify which company to access.
            Ignored for standard company API keys.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicAttachRunContactsDto'
      responses:
        '201':
          description: Contacts attached
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicAttachRunContactsResponseDto'
        '402':
          description: >-
            Payment Required - the batch costs more than the balance allows, or
            there is no active subscription. All-or-nothing: nothing is
            dispatched and the run stays a draft.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingRefusalResponse'
        '422':
          description: >-
            Unprocessable Entity - the run is cancelled, or a contact is not in
            this company.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearer: []
components:
  schemas:
    PublicAttachRunContactsDto:
      type: object
      properties:
        contactIds:
          type: array
          description: >-
            Contact ids to attach. Max 500 per request; send more in separate
            requests, which is safe because attaching an id twice is a no-op.
          items:
            type: string
            format: uuid
          minItems: 1
          maxItems: 500
          example:
            - 123e4567-e89b-12d3-a456-426614174000
      required:
        - contactIds
    PublicAttachRunContactsResponseDto:
      type: object
      properties:
        attached:
          type: number
          description: Contacts newly attached by this request
          example: 8
        skipped:
          type: number
          description: >-
            Ids already on the run, so this request did nothing for them.
            Repeating a batch is safe.
          example: 2
        totalContacts:
          type: number
          description: Contacts on the run after this request
          example: 10
        queued:
          type: number
          description: >-
            Conversations queued by this attach. Non-zero only on an
            already-launched run; zero on a draft, which dials nothing until
            launched.
          example: 8
      required:
        - attached
        - skipped
        - totalContacts
        - queued
    BillingRefusalResponse:
      type: object
      description: >-
        A 402 refusal. `billing` is present when the billing system reported
        figures, and absent when it had none to report - treat an absent object
        as unknown rather than as zero.
      properties:
        statusCode:
          type: integer
          example: 402
        message:
          type: string
          example: >-
            Insufficient minutes available to schedule 10 interviews (50 minutes
            total). Please purchase additional minutes.
        error:
          type: string
          example: PAYMENT_REQUIRED
        billing:
          type: object
          description: >-
            The shortfall, in the company's own billing unit. Read
            `billingSystem` first: a minutes-based company carries
            `requiredMinutes` and `availableMinutes`, a credits-based one
            `requiredCredits` and `availableCredits`, matching the split `GET
            /billing/usage` returns.
          properties:
            billingSystem:
              type: string
              enum:
                - minutes
                - credits
              example: minutes
            requiredMinutes:
              type: number
              description: >-
                Minutes the request would cost. Present only for a minutes-based
                company.
              example: 50
            availableMinutes:
              type: number
              description: >-
                Minutes the company has left. Present only for a minutes-based
                company.
              example: 12
            requiredCredits:
              type: number
              description: >-
                Credits the request would cost. Present only for a credits-based
                company.
              example: 50
            availableCredits:
              type: number
              description: >-
                Credits the company has left. Present only for a credits-based
                company.
              example: 12
            suggestedAction:
              type: string
              enum:
                - purchase_minutes
                - upgrade_plan
                - increase_spending_cap
                - contact_support
              example: purchase_minutes
          required:
            - billingSystem
        traceId:
          type: string
          example: 4b1f0c9d2e6a47f8b3c5d7e9a1b2c3d4
      required:
        - statusCode
        - message
    ErrorResponse:
      type: object
      properties:
        statusCode:
          type: integer
          example: 400
        message:
          type: string
          example: Validation failed
        error:
          type: string
          example: Bad Request
      required:
        - statusCode
        - message
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: API key for authentication using Bearer scheme

````