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: 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.