politicalcomms-com Conversations API

Read inbound threads and reply inside them from the same number. Conversations are created by project sends; the API never starts one.

Operations 4

GET /conversations List Conversations #
GET /conversations/{conversation_id} Get Conversation #
GET /conversations/{conversation_id}/messages List Conversation Messages #
POST /conversations/{conversation_id}/messages Reply to Conversation #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/politicalcomms-com:politicalcomms-com-conversations-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

politicalcomms-com-conversations-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Political Comms Conversations API
  summary: Direct-to-carrier political texting API for campaigns, PACs, advocacy organizations, fundraisers, and elected officials.
  description: 'Public REST API for the Political Comms platform. Surfaces include Projects (compose, test, schedule, send), Conversations (read inbound threads and reply inside them), Contact Lists (S3 import and analysis), Media Files, Organizations and hierarchy, Brands, Campaigns, Tracking Domains, Phone Numbers, Analytics, and Billing.


    Authentication is an API key passed in the `X-API-Key` header. Keys are generated from the dashboard at Admin → API and are prefixed `pc_live_`. All POST and PATCH endpoints that mutate state are designed to be safe to retry, with optional `Idempotency-Key` headers for stronger guarantees. Rate limit is 600 requests per minute per key (bursts up to 600, refilling at 10 per second, the same for every scope; the `X-RateLimit-Limit` header reports your key''s exact limit, and `Retry-After` on a 429 is the seconds until your next request is allowed).


    Webhooks emit `message.sent`, `message.delivered`, `message.failed`, `message.replied`, and `link.clicked` events. Payloads are HMAC-signed; validate the signature before trusting any payload.


    A Model Context Protocol (MCP) server is available at https://docs.politicalcomms.com/mcp for AI agents that need to search the documentation programmatically. The developer hub at https://politicalcomms.com/developers/ has quickstart examples in cURL, raw HTTP, and Python.'
  version: 1.4.1
  termsOfService: https://politicalcomms.com/terms/
  contact:
    name: Political Comms Support
    email: support@politicalcomms.com
    url: https://docs.politicalcomms.com
  license:
    name: Proprietary
    url: https://politicalcomms.com/terms/
  x-logo:
    url: https://politicalcomms.com/images/brand/pcomms-logo-left-of-text.png
    altText: Political Comms
    backgroundColor: '#ffffff'
    href: https://politicalcomms.com/
  x-mcp:
    url: https://docs.politicalcomms.com/mcp
    discovery_url: https://docs.politicalcomms.com/.well-known/mcp
    transport: http
    auth: none
    tools:
    - search_political_comms
    - query_docs_filesystem_political_comms
    - submit_feedback
servers:
- url: https://api.politicalcomms.com/v1
  description: Production
security:
- ApiKeyAuth: []
tags:
- name: Conversations
  description: Read inbound threads and reply inside them from the same number. Conversations are created by project sends; the API never starts one.
paths:
  /conversations:
    get:
      tags:
      - Conversations
      summary: List Conversations
      operationId: listConversations
      description: 'Conversations with at least one inbound message, across every organization the key can access, newest inbound first. Built for one job: recovering the inbound messages you missed while your `message.replied` webhook endpoint was down. Advance `updated_since` to the newest `last_inbound_at` you have processed and poll no more than once a minute; the webhook remains the real-time path.


        Keyset paginated: page until `next_cursor` is null. Seed and test conversations are excluded (pass `include_test=true` to include test threads). There is no `status` filter; every row carries `status`.'
      parameters:
      - name: project_id
        in: query
        required: false
        schema:
          type: string
          format: uuid
        description: Only conversations on this project.
      - name: updated_since
        in: query
        required: false
        schema:
          type: string
          format: date-time
        description: Only conversations whose `last_inbound_at` is at or after this instant. Defaults to 7 days ago. More than 90 days ago is rejected with `400`.
      - name: include_test
        in: query
        required: false
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
          default: 'false'
        description: Include test conversations.
      - $ref: '#/components/parameters/Limit'
      - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of conversations.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Conversation'
                      has_more:
                        type: boolean
                      next_cursor:
                        type:
                        - string
                        - 'null'
                    required:
                    - data
                    - has_more
                    - next_cursor
                    additionalProperties: true
                required:
                - success
                - data
                additionalProperties: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /conversations/{conversation_id}:
    get:
      tags:
      - Conversations
      summary: Get Conversation
      operationId: getConversation
      description: Fetch one conversation. Returns `404 CONVERSATION_NOT_FOUND` both when the id does not exist and when it belongs to an organization this key cannot access.
      parameters:
      - $ref: '#/components/parameters/ConversationIdPath'
      responses:
        '200':
          description: The conversation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Conversation'
                required:
                - success
                - data
                additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /conversations/{conversation_id}/messages:
    get:
      tags:
      - Conversations
      summary: List Conversation Messages
      operationId: listConversationMessages
      description: 'The thread, newest first, both directions. Reading it has no side effects: it does not mark the thread read in the dashboard. Keyset paginated: page until `next_cursor` is null.'
      parameters:
      - $ref: '#/components/parameters/ConversationIdPath'
      - $ref: '#/components/parameters/Limit'
      - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of messages.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/ConversationMessage'
                      has_more:
                        type: boolean
                      next_cursor:
                        type:
                        - string
                        - 'null'
                    required:
                    - data
                    - has_more
                    - next_cursor
                    additionalProperties: true
                required:
                - success
                - data
                additionalProperties: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
      - Conversations
      summary: Reply to Conversation
      operationId: replyToConversation
      description: 'Send a text reply inside an existing conversation: same number, same contact, same project. Typical use: answer a `message.replied` webhook with your own content.


        The reply is accepted (`202`) and queued at high priority; its final state arrives on the `message.sent`, `message.delivered`, or `message.failed` webhook carrying the returned `message_id`. Replies are SMS only (up to 1600 characters, billed per segment at the number''s outbound rate) and are not subject to quiet hours. Every rejection happens before any charge.


        Send an `Idempotency-Key` on every call you might retry. A `503 SEND_ENQUEUE_FAILED` means nothing was sent and nothing was charged: retry the same call. On any other 5xx, read `GET /conversations/{conversation_id}/messages` and look for your text before retrying.'
      parameters:
      - $ref: '#/components/parameters/ConversationIdPath'
      - $ref: '#/components/parameters/IdempotencyKeyHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - text
              properties:
                text:
                  type: string
                  minLength: 1
                  maxLength: 1600
                  description: The reply body. Unknown properties are rejected with `400`; media is not accepted.
              additionalProperties: false
            example:
              text: Thanks for reaching out. Polls are open until 7pm at Lincoln Elementary.
      responses:
        '202':
          description: Reply accepted and queued for delivery.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/ConversationReplyResult'
                required:
                - success
                - data
                additionalProperties: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '409':
          description: 'The conversation cannot take a reply: `CONTACT_OPTED_OUT` (the contact replied STOP), `CONVERSATION_NOT_SENDABLE` (the thread has no sending number), `PROJECT_DELETED`, `PHONE_NUMBER_UNAVAILABLE` (the thread''s number was released or is inactive), or `SENDING_PAUSED` (sending is paused for the organization or platform-wide). Nothing was charged for CONTACT_OPTED_OUT, CONVERSATION_NOT_SENDABLE, PROJECT_DELETED, PHONE_NUMBER_UNAVAILABLE, or SENDING_PAUSED.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                conversationNotSendable:
                  summary: The conversation cannot take a reply right now
                  value:
                    success: false
                    error: Contact has opted out of this conversation
                    code: CONTACT_OPTED_OUT
                    statusCode: 409
                organizationPaused:
                  summary: Sending is paused for this organization
                  value:
                    error: Sending is currently paused for this organization
                    code: SENDING_PAUSED
                    details:
                      scope: organization
                platformPaused:
                  summary: Sending is paused platform-wide
                  value:
                    error: Sending is currently paused
                    code: SENDING_PAUSED
                    details:
                      scope: platform
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  parameters:
    ConversationIdPath:
      name: conversation_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Conversation ID, as delivered on the `message.replied` webhook (`conversation_id`) or returned by `GET /conversations`.
    Cursor:
      name: cursor
      in: query
      required: false
      description: Opaque cursor from the previous page's `next_cursor`. Never parse or construct one; a cursor from one list is rejected by another.
      schema:
        type: string
        maxLength: 4096
    Limit:
      name: limit
      in: query
      required: false
      description: Rows per page, 1-200.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    IdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        minLength: 16
        maxLength: 200
      description: 'Optional idempotency key: a unique string of 16-200 printable ASCII characters (a UUID is recommended). Retrying the write with the same key within 24 hours returns the stored response of the first call with an `X-Idempotent-Replayed: true` response header instead of executing it again. Reusing a key with a different request body returns `422` (`IDEMPOTENCY_MISMATCH`); a duplicate sent while the first call is still running returns `409` with a `Retry-After` header. Keys are scoped per endpoint and organization.'
  responses:
    Forbidden:
      description: The API key lacks the required permission scope, or the requested resource belongs to an organization outside the key's hierarchy.
      content:
        application/json:
          schema:
            oneOf:
            - $ref: '#/components/schemas/AuthErrorResponse'
            - $ref: '#/components/schemas/ErrorResponse'
          examples:
            missingScope:
              summary: Key lacks the required permission scope
              value:
                error: Insufficient permissions
                message: 'This API key does not have the required permissions: public:read'
            orgAccessDenied:
              summary: Resource outside the key's organization hierarchy
              value:
                error: 'Access denied: You do not have permission to access this organization''s data'
                code: ORG_ACCESS_DENIED
                correlationId: de038205-7c9c-4d4a-99bc-d7275a52f07e
    PaymentRequired:
      description: Wallet balance cannot cover the operation (`INSUFFICIENT_BALANCE`). Add funds and retry.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    BadRequest:
      description: Invalid request (validation failure, malformed parameters, or a request the resource state does not allow).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: Validation failed
            code: VALIDATION_ERROR
            correlationId: 030bf857-922f-4b82-8fca-c5b0769c1590
            details:
            - field: organization_id
              message: Invalid UUID
              code: invalid_format
    Unauthorized:
      description: Missing, malformed, revoked, or expired API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AuthErrorResponse'
          example:
            error: API key required
            message: Please provide an API key in the X-API-Key header
    NotFound:
      description: Resource not found or not visible to this API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: Project not found
            code: PROJECT_NOT_FOUND
            correlationId: de038205-7c9c-4d4a-99bc-d7275a52f07e
    ServiceUnavailable:
      description: 'The request could not be completed right now and had no effect. For `SEND_ENQUEUE_FAILED` nothing was sent and nothing was charged: retry the same call.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InternalError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: An unexpected error occurred. Please try again later.
            code: INTERNAL_ERROR
            correlationId: de038205-7c9c-4d4a-99bc-d7275a52f07e
    RateLimited:
      description: Rate limit exceeded. Check the Retry-After header.
      headers:
        Retry-After:
          description: Seconds until the window resets.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitErrorResponse'
          example:
            success: false
            error: Rate limit exceeded
            message: Maximum 600 requests per 60 seconds
            code: RATE_LIMIT_EXCEEDED
            statusCode: 429
            retryAfter: 3
  schemas:
    RateLimitErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          enum:
          - false
        error:
          type: string
        message:
          type: string
        code:
          type: string
          enum:
          - RATE_LIMIT_EXCEEDED
        statusCode:
          type: integer
          enum:
          - 429
        retryAfter:
          type: integer
          description: Seconds until the rate-limit window resets. Also sent as the Retry-After header.
      required:
      - success
      - error
      - code
      - statusCode
      - retryAfter
      additionalProperties: true
    ConversationMessage:
      type: object
      properties:
        message_id:
          type: string
          format: uuid
        direction:
          type: string
          enum:
          - inbound
          - outbound
        text:
          type: string
        media_urls:
          type: array
          items:
            type: string
            format: uri
        status:
          type: string
          enum:
          - unsent
          - queued
          - sending
          - sent
          - delivered
          - failed
          - received
          - on_hold
        sent_at:
          type:
          - string
          - 'null'
          format: date-time
        delivered_at:
          type:
          - string
          - 'null'
          format: date-time
        received_at:
          type:
          - string
          - 'null'
          format: date-time
          description: Arrival time of an inbound message; always null on outbound rows.
        error_code:
          type:
          - string
          - 'null'
          description: Delivery error code for a failed outbound message, in the same display form as the message.failed webhook and CSV exports (e.g. "300" opted-out, "012" invalid destination; see Delivery error codes). Treat it as an opaque string. null when the message did not fail, or when the failure was a platform-side outcome rather than a delivery result.
        carrier:
          type:
          - string
          - 'null'
          description: 'The contact''s mobile carrier: the recipient''s on an outbound message, the sender''s on an inbound one. The three national networks are exactly `AT&T`, `Verizon`, or `T-Mobile` (brands and subsidiaries are reported under the network they run on, e.g. Cricket under `AT&T`, Straight Talk under `Verizon`, Mint Mobile under `T-Mobile`). Any other carrier is reported under its own registered name (e.g. `CELLULAR SOUTH, INC.`); treat it as an opaque string. null when the carrier is not known, and on inbound messages received before this field was introduced.'
      required:
      - message_id
      - direction
      - text
      - media_urls
      - status
      additionalProperties: true
    Conversation:
      type: object
      description: One thread between one of the organization's sending numbers and one contact. Created by a project send (broadcast, survey, or test); never by the API.
      properties:
        conversation_id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        from:
          type: string
          description: The organization's sending number (E.164). Every reply goes out from this number.
        to:
          type: string
          description: The contact's number (E.164).
        status:
          type: string
          enum:
          - active
          - inactive
          - opted_out
        opted_out:
          type: boolean
          description: True once the contact replied STOP. Replies to an opted-out thread are refused with `409 CONTACT_OPTED_OUT`.
        is_test:
          type: boolean
        contact:
          type: object
          properties:
            phone_number:
              type: string
          required:
          - phone_number
          additionalProperties: true
        inbound_messages:
          type: integer
        outbound_messages:
          type: integer
        first_message_at:
          type:
          - string
          - 'null'
          format: date-time
        last_inbound_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When the contact last texted in. The list endpoint sorts and filters on this.
        last_outbound_at:
          type:
          - string
          - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
      required:
      - conversation_id
      - project_id
      - from
      - to
      - status
      - opted_out
      - is_test
      - contact
      - inbound_messages
      - outbound_messages
      - created_at
      additionalProperties: true
    ConversationReplyResult:
      type: object
      properties:
        message_id:
          type: string
          format: uuid
          description: The queued outbound message. `message.sent` / `message.delivered` / `message.failed` webhooks for it carry this id.
        conversation_id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        from:
          type: string
        to:
          type: string
        text:
          type: string
          description: The text as it will be sent (normalized for GSM encoding).
        created_at:
          type: string
          format: date-time
      required:
      - message_id
      - conversation_id
      - project_id
      - from
      - to
      - text
      - created_at
      additionalProperties: true
    AuthErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Short error summary.
        message:
          type: string
          description: Human-readable detail.
      required:
      - error
      - message
      additionalProperties: true
      description: Error body returned by the API-key authentication layer (401 and permission 403s).
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message.
        code:
          type: string
          description: Machine-readable error code, e.g. VALIDATION_ERROR, ORG_ACCESS_DENIED, PROJECT_NOT_FOUND.
        correlationId:
          type: string
          description: Request correlation ID; include it in support requests.
        details:
          type:
          - array
          - object
          description: Present on some errors. For VALIDATION_ERROR, an array of { field, message, code } with one entry per rejected field (field is the dotted path); an object for other codes (e.g. insufficient-balance shortfall).
      required:
      - error
      - code
      additionalProperties: true
      description: Standard error body produced by the API error handler for 4xx/5xx responses.
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Authenticate every request by passing your API key in the X-API-Key header. Keys are scoped to your organization hierarchy.