curl --request POST \
--url https://api.politicalcomms.com/v1/projects \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"organization_id": "01HX0000000000000000000000",
"channel": "10dlc",
"brand_id": "01HX0000000000000000000001",
"campaign_id": "01HX0000000000000000000002",
"phone_number_ids": [
"01HX0000000000000000000003",
"01HX0000000000000000000006"
],
"name": "Spring Outreach",
"protocol": "sms",
"contact_list_ids": [
"01HX0000000000000000000004"
],
"suppression_list_ids": [],
"message_text": "Hi {first_name}, early voting starts Monday. More info: {link}",
"media_ids": [],
"link_tracking_enabled": true,
"link_tracking_destination_url": "https://example.com/vote",
"link_tracking_domain_id": "01HX0000000000000000000005",
"link_tracking_param_field": "voter_id",
"link_tracking_fallback_url": null,
"opt_out_footer_enabled": true
}
'{
"success": true,
"data": {
"project_id": "01HX000000000000000000P001",
"name": "Spring Outreach",
"type": "broadcast",
"status": "awaiting_test",
"channel": "10dlc",
"created_via_api": true,
"estimated_cost_cents": 4250,
"total_recipients": 500,
"completeness": {
"has_list": true,
"has_message": true,
"has_phone_number": true,
"ready_to_test": true
}
}
}Create Project
Create a new project. type selects the payload shape: broadcast (the default) sends one message to the audience; survey runs a multi-question flow with branching.
Phone numbers. Assign one or more sending numbers via phone_number_ids (up to 49). The project spreads new conversations randomly across the assigned numbers, and once a recipient has been messaged from a given number every later message to that recipient comes from the same number (sticky sender). A single legacy phone_number_id is still accepted and is treated as a one-element phone_number_ids.
Channel. channel defaults to 10dlc. For broadcast projects a 10dlc channel requires brand_id + campaign_id (and toll_free_verification_id must be omitted), while toll-free requires toll_free_verification_id with matching toll-free phone_number_ids (brand_id/campaign_id omitted). Survey projects always require campaign_id, on both channels.
Surveys. questions defines the whole flow: an intro at sequence 1, then multiple_choice/open_ended questions, optionally ending with an outro. Multiple-choice options can branch with next_sequence, and no_match_sequence routes unmatched replies; both are forward-only. Each question can be SMS or MMS (message_type + media_ids). Question text supports merge tags and {tracking_url}. Surveys need at least 2 questions and 1 phone number.
When link_tracking_enabled is true, link_tracking_destination_url and link_tracking_domain_id become required. The destination URL may embed the selected link_tracking_param_field anywhere via a placeholder named after it, e.g. https://test.com?utm_content=xyzd_{linkid} redirects as ...utm_content=xyzd_ABC123 (no separate &linkid= pair is appended). Without a placeholder the field is appended as its own query pair. link_tracking_destination_url may also be exactly https://{<link_tracking_param_field>}, e.g. https://{custom_url}; each recipient’s tracking link then redirects to the URL stored in that contact field instead (short_id is still appended as a query param, but no <field> query param is). In this mode link_tracking_fallback_url is required, and recipients whose field is empty or not a valid URL go there instead. phone cannot be used as a whole-URL destination, and a {field} token in the host position must be the entire URL (https://{custom_url}/path is rejected). A placeholder that does not match the selected field, or an invalid whole-URL form, is rejected with a 400 (INVALID_LINK_PLACEHOLDER).
Opt-out footer. Broadcast messages automatically carry the STOP=END opt-out footer. Set opt_out_footer_enabled to false to disable it (defaults to true); surveys never carry the footer.
Drafts. contact_list_ids is optional. With at least one list the created project starts in awaiting_test status (send a test, then schedule). Without it the project is created as a draft; attach lists later via PATCH /projects/{id} and the project moves to awaiting_test automatically. Drafts cannot be tested or scheduled. The response’s completeness block shows what is still missing.
curl --request POST \
--url https://api.politicalcomms.com/v1/projects \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"organization_id": "01HX0000000000000000000000",
"channel": "10dlc",
"brand_id": "01HX0000000000000000000001",
"campaign_id": "01HX0000000000000000000002",
"phone_number_ids": [
"01HX0000000000000000000003",
"01HX0000000000000000000006"
],
"name": "Spring Outreach",
"protocol": "sms",
"contact_list_ids": [
"01HX0000000000000000000004"
],
"suppression_list_ids": [],
"message_text": "Hi {first_name}, early voting starts Monday. More info: {link}",
"media_ids": [],
"link_tracking_enabled": true,
"link_tracking_destination_url": "https://example.com/vote",
"link_tracking_domain_id": "01HX0000000000000000000005",
"link_tracking_param_field": "voter_id",
"link_tracking_fallback_url": null,
"opt_out_footer_enabled": true
}
'{
"success": true,
"data": {
"project_id": "01HX000000000000000000P001",
"name": "Spring Outreach",
"type": "broadcast",
"status": "awaiting_test",
"channel": "10dlc",
"created_via_api": true,
"estimated_cost_cents": 4250,
"total_recipients": 500,
"completeness": {
"has_list": true,
"has_message": true,
"has_phone_number": true,
"ready_to_test": true
}
}
}Authorizations
Authenticate every request by passing your API key in the X-API-Key header. Keys are scoped to your organization hierarchy.
Headers
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.
16 - 200Body
- Broadcast project
- Survey project
Unknown body properties are rejected with a 400 (strict validation).
sms, mms Project type. Optional for broadcast (the default).
broadcast Messaging channel. Defaults to 10dlc.
10dlc, toll-free Required when channel=10dlc; omit when channel=toll-free.
Required when channel=10dlc; omit when channel=toll-free.
Required when channel=toll-free; omit when channel=10dlc. Must match the verification behind the chosen phone_number_ids.
Sending phone number IDs (1-49). At least one of phone_number_ids or phone_number_id is required. New conversations are spread randomly across the numbers; each recipient is then pinned to one number.
1 - 49 elementsDeprecated. Use phone_number_ids. A single id is accepted and treated as a one-element phone_number_ids. Provide one of phone_number_ids or phone_number_id.
Optional. Omit to create the project as a draft with no audience; attach lists later via PATCH /projects/{id}. When present, must contain at least one list id.
1Where tracking links redirect. May embed the selected link parameter anywhere via a placeholder named after it, e.g. 'https://test.com?utm_content=xyzd_{linkid}' redirects as '...utm_content=xyzd_ABC123' (URL-encoded value, empty when the contact has none; the parameter is then not appended separately). A placeholder that does not match link_tracking_param_field is rejected with a 400 (INVALID_LINK_PLACEHOLDER). Without a placeholder the parameter is appended as its own query pair. May also be exactly 'https://{<link_tracking_param_field>}' to send each recipient to the URL in that contact field; then link_tracking_fallback_url is required. '{phone}' is not allowed as a whole URL.
Contact field carried on tracking-link redirects. Use 'phone', a contact custom-field name, or omit for no param (default). By default the field's name becomes an appended query-param key with the contact's value; a matching {field} placeholder in link_tracking_destination_url embeds the value there instead.
64Redirect used when a recipient's link_tracking_param_field value is empty or not a valid URL. Required when link_tracking_destination_url is a whole-URL placeholder (e.g. 'https://{custom_url}'); rejected with a 400 (INVALID_LINK_PLACEHOLDER) otherwise.
Whether the 'STOP=END' opt-out footer is appended to every outbound message. Defaults to true. Broadcast projects only; surveys never carry the footer.
