{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/hellou-ai/json-schema/hellou-ai-voice-campaign-schema.json",
  "title": "voice_campaign",
  "description": "A voice campaign. Outbound campaigns dial the contacts of their audience; inbound campaigns put\ntheir agents on PBX queues while the agents are available.\n",
  "x-generated": "2026-11-11",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/hellou-ai-openapi.json#/components/schemas/voice_campaign",
  "type": "object",
  "properties": {
    "id": {
      "type": "integer",
      "description": "The campaign's number within the account. Use it in the campaign paths."
    },
    "title": {
      "type": "string"
    },
    "description": {
      "type": [
        "string",
        "null"
      ]
    },
    "status": {
      "type": "string",
      "enum": [
        "draft",
        "validating",
        "scheduled",
        "running",
        "paused",
        "completed",
        "cancelled",
        "failed"
      ]
    },
    "handler_type": {
      "type": "string",
      "enum": [
        "voice_human",
        "voice_ai"
      ],
      "description": "Who handles the calls."
    },
    "direction": {
      "type": "string",
      "enum": [
        "outbound",
        "inbound"
      ]
    },
    "inbound_queue_ids": {
      "type": "array",
      "description": "For inbound campaigns, the PBX hunt-group ids its agents are logged in to while available. Empty for outbound campaigns.",
      "items": {
        "type": "string"
      }
    },
    "executor_type": {
      "type": "string",
      "enum": [
        "team",
        "agent",
        "ai_agent",
        "agents"
      ],
      "description": "Whether a team, one agent, a hand-picked list of agents or an AI agent works the campaign."
    },
    "executor_id": {
      "type": [
        "integer",
        "null"
      ]
    },
    "agent_user_ids": {
      "type": "array",
      "description": "The agents on the list when `executor_type` is `agents`.",
      "items": {
        "type": "integer"
      }
    },
    "dialer_mode": {
      "type": "string",
      "enum": [
        "preview",
        "progressive",
        "power"
      ],
      "description": "How an outbound campaign places calls. Ignored by inbound campaigns."
    },
    "lines_per_agent": {
      "type": "number",
      "minimum": 1,
      "maximum": 3,
      "default": 1,
      "description": "Calls placed per free agent on a human outbound power campaign. Only 1 is accepted until multi-line dialing ships; any other value answers 422 (`attributes` lists `lines_per_agent`)."
    },
    "retry_after_voicemail": {
      "type": "boolean",
      "default": true,
      "description": "Whether a contact is called again after a voicemail (`true` by default). With `false`, a voicemail completes the contact with no more attempts and the `voicemail` disposition (or the agent's voicemail disposition). Only a real voicemail signal counts: answering-machine detection, or a voicemail disposition from the agent or the AI agent; a billed call without one is retried as before."
    },
    "max_attempts": {
      "type": "integer",
      "description": "Calls to one contact before it is given up. Once the campaign has calls it can only be raised (422 `attempts_lowered`)."
    },
    "retry_delay_mins": {
      "type": "integer",
      "description": "Minutes before a contact who was not reached is called again."
    },
    "after_call_work_time": {
      "type": "integer",
      "description": "Seconds an agent has for wrap-up after each call, from 10 to 3600. A value outside that range answers 422 (`attributes` lists `after_call_work_time`)."
    },
    "import_status": {
      "type": "string",
      "enum": [
        "idle",
        "importing",
        "failed"
      ],
      "description": "Where the contact file import is. `importing` while a file uploaded with `contacts/upload_csv` is read, inserted and validated; `failed` when it was refused (`import_error` says why, the contacts are unchanged); `idle` otherwise, including once the contacts are validated."
    },
    "import_error": {
      "type": [
        "string",
        "null"
      ],
      "description": "Why the last file import failed, in the language of the request that uploaded it. `null` unless `import_status` is `failed`."
    },
    "import_progress": {
      "type": [
        "integer",
        "null"
      ],
      "minimum": 0,
      "maximum": 100,
      "description": "While `import_status` is `importing`, how far the validation of the imported contacts is, in percent. `null` before it starts and when idle."
    },
    "total_contacts": {
      "type": "integer"
    },
    "contacts_pending": {
      "type": "integer"
    },
    "contacts_called": {
      "type": "integer"
    },
    "contacts_answered": {
      "type": "integer"
    },
    "contacts_completed": {
      "type": "integer"
    },
    "contacts_failed": {
      "type": "integer"
    },
    "scheduled_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "The instant the campaign was scheduled to start, in UTC. It may be in the past: a campaign whose time has come is launched, and keeps the stamp."
    },
    "ends_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "When the campaign ends, in UTC, or null when it has no end date. Once it has passed, the campaign completes; a scheduled campaign past it never launches."
    },
    "started_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time"
    },
    "completed_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time"
    },
    "archived_at": {
      "type": [
        "string",
        "null"
      ],
      "format": "date-time",
      "description": "When the campaign was archived, or null while it is not archived."
    },
    "test": {
      "type": "boolean",
      "description": "A label only: a test campaign places real calls like any other, and the label only keeps it out of the default lists unless `include_test` is set. It can change only while the campaign is a draft; on any other status a change answers 422 `reason: locked_fields`."
    },
    "voice_line_id": {
      "type": [
        "integer",
        "null"
      ],
      "description": "For an AI voice campaign, the AI phone line whose AI agent places the calls and whose number the contacts see."
    },
    "voice_line": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "id": {
          "type": "integer"
        },
        "name": {
          "type": [
            "string",
            "null"
          ]
        },
        "phone_number": {
          "type": "string"
        },
        "agent_bot_id": {
          "type": "integer"
        }
      }
    },
    "program_id": {
      "type": [
        "integer",
        "null"
      ],
      "description": "The voice campaign program this campaign belongs to, or `null`."
    },
    "batch_label": {
      "type": [
        "string",
        "null"
      ],
      "description": "The batch of the program this campaign is part of."
    },
    "sweep_number": {
      "type": [
        "integer",
        "null"
      ],
      "description": "Which sweep (pass over the audience) of the batch this campaign is, from 1."
    },
    "callbacks_pending": {
      "type": "integer",
      "description": "Contacts waiting for a call back they asked for, due or not. A completed campaign that still holds some is reopened when one falls due; one nobody dialled for 7 days is given up."
    },
    "paused_reason": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "ai_minutes_cap",
        null
      ],
      "description": "Why the system paused an AI campaign (`ai_minutes_cap`, the AI phone minutes cap was reached), or `null`. Resuming the campaign clears it."
    },
    "ai_settings": {
      "description": "What the AI agent says and records on this campaign's calls (AI voice campaigns only).",
      "anyOf": [
        {
          "$ref": "#/$defs/voice_campaign_ai_settings"
        },
        {
          "type": "null"
        }
      ]
    },
    "consent_attestation": {
      "type": [
        "object",
        "null"
      ],
      "description": "Who attested, and when, that the contacts consented to calls from an AI agent (AI voice campaigns).",
      "properties": {
        "user_id": {
          "type": "integer"
        },
        "at": {
          "type": "string",
          "format": "date-time"
        },
        "text_version": {
          "type": "integer"
        }
      }
    },
    "created_at": {
      "type": "string",
      "format": "date-time"
    },
    "updated_at": {
      "type": "string",
      "format": "date-time"
    },
    "declared_variables": {
      "type": "array",
      "description": "Only on the single-campaign response. The AI agent's variables that the contact file feeds, in the order the agent\ndeclares them. Empty for a campaign worked by people. `label` is in the requester's language.\n",
      "items": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "The variable's type, for example `string`, `number`, `date`, `boolean` or `enum`."
          },
          "options": {
            "type": "array",
            "description": "The allowed values of an `enum` variable.",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "key",
          "label",
          "type",
          "options"
        ]
      }
    },
    "variable_map": {
      "type": "object",
      "description": "Only on the single-campaign response. Which declared variable each column of the contact file feeds, as\n`{ \"<file header>\": \"<variable key>\" }`: the saved mapping plus, for a variable it does not name, the file\ncolumn whose own name reads as the key (the way a call reads it). Empty for a campaign worked by people.\n",
      "additionalProperties": {
        "type": "string"
      }
    }
  },
  "$defs": {
    "voice_campaign_ai_settings": {
      "type": "object",
      "description": "The settings of an AI voice campaign. Texts may name the call's data: `{{contact.name}}`, `{{contact.first_name}}`,\n`{{campaign.vars.<column>}}` (a column of the uploaded contact list, its header in lower case with\n`_` for spaces), `{{lookup.<key>}}` (the AI agent's pre-call lookup) and `{{call.number}}`.\n",
      "properties": {
        "opening": {
          "type": "object",
          "description": "What the AI agent says when the contact answers, per language (`es`, `en`). Empty uses the AI agent's own opening.",
          "additionalProperties": {
            "type": "string"
          }
        },
        "first_speaker": {
          "type": "string",
          "enum": [
            "",
            "agent",
            "caller"
          ],
          "description": "Who opens the call. Empty follows the AI agent's own voice section; `agent` makes it speak first, as it does today; `caller` makes it wait for the contact to speak, greet nobody, ask whether it reached the person, and say the recording notice only after that answer."
        },
        "dispositions": {
          "type": "array",
          "maxItems": 20,
          "description": "The outcomes the AI agent may record. `outcome` decides what the dialer does next; `next_in_group` means the person who answered is not the one the call is for, so the next person of the contact's group is called instead.",
          "items": {
            "type": "object",
            "required": [
              "key",
              "label",
              "outcome"
            ],
            "properties": {
              "key": {
                "type": "string",
                "pattern": "^[a-z][a-z0-9_]{0,39}$"
              },
              "label": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "outcome": {
                "type": "string",
                "enum": [
                  "contacted",
                  "retry",
                  "closed",
                  "do_not_call",
                  "next_in_group"
                ]
              }
            }
          }
        },
        "extraction_fields": {
          "type": "array",
          "maxItems": 15,
          "description": "The facts the AI agent collects during the call.",
          "items": {
            "type": "object",
            "required": [
              "key",
              "label",
              "type"
            ],
            "properties": {
              "key": {
                "type": "string",
                "pattern": "^[a-z][a-z0-9_]{0,39}$"
              },
              "label": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "string",
                  "number",
                  "boolean",
                  "date",
                  "enum"
                ]
              },
              "options": {
                "type": "array",
                "description": "The allowed values of an `enum` field.",
                "items": {
                  "type": "string"
                }
              },
              "description": {
                "type": "string"
              }
            }
          }
        },
        "voicemail": {
          "type": "object",
          "properties": {
            "action": {
              "type": "string",
              "enum": [
                "hang_up",
                "leave_message"
              ]
            },
            "message": {
              "type": "object",
              "description": "The message left on a voicemail, per language.",
              "additionalProperties": {
                "type": "string"
              }
            }
          }
        },
        "hot_lead": {
          "type": "object",
          "properties": {
            "route_key": {
              "type": [
                "string",
                "null"
              ],
              "description": "The AI agent's handover route an interested contact is transferred to, live."
            }
          }
        },
        "ringing_seconds": {
          "type": "integer",
          "minimum": 15,
          "maximum": 60
        },
        "variable_map": {
          "type": "object",
          "maxProperties": 50,
          "description": "Which column of the contact list feeds each of the AI agent's declared variables, column header to variable key. Columns it does not name are matched by name.",
          "additionalProperties": {
            "type": "string"
          }
        },
        "whatsapp_follow_up": {
          "description": "Overrides any key of the AI agent's WhatsApp follow-up (its voice section's `whatsapp_follow_up`) for this campaign's calls; an empty value follows the AI agent. Check a configuration with `POST /voice_follow_ups/check`.",
          "allOf": [
            {
              "$ref": "#/$defs/voice_follow_up_config"
            }
          ]
        }
      }
    },
    "voice_follow_up_config": {
      "type": "object",
      "description": "The WhatsApp message an AI agent may send after the contact says yes on a phone call. Set on the AI agent's\n`voice` section as `whatsapp_follow_up`; a voice campaign may override any key in its `ai_settings.whatsapp_follow_up`\n(an empty value follows the AI agent). The AI agent never chooses the number, the text or the template: the message\ngoes to the call's number, as set here.\n",
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Whether the AI agent offers it."
        },
        "inbox_id": {
          "type": [
            "integer",
            "null"
          ],
          "description": "The WhatsApp inbox that sends it."
        },
        "template_name": {
          "type": "string",
          "description": "An approved template of that inbox, with variables in the body only."
        },
        "template_language": {
          "type": "string"
        },
        "params": {
          "type": "object",
          "description": "Each body variable of the template and where its value comes from: `contact.first_name`, `contact.name`, `campaign.name`, `campaign.vars.<column>` (a column of the campaign's contact list), `call.direction`, `call.line`, `call.number`, or `literal:<text>`.",
          "additionalProperties": {
            "type": "string"
          }
        },
        "offer": {
          "type": "object",
          "description": "What the AI agent says to offer it, per language (`es`, `en`). Must name the business and WhatsApp and must not promise that the message arrives.",
          "additionalProperties": {
            "type": "string"
          }
        },
        "skip_dispositions": {
          "type": "array",
          "description": "Campaign outcomes that never get it. The wrong person (`next_in_group`) never does either.",
          "items": {
            "type": "string"
          }
        }
      }
    }
  }
}