Docs/Sendify Direct and APIAPIReference
Sendify API reference
Every endpoint is open on this page with request fields, a ready to copy request, and the exact response you get back. Nothing is hidden behind a dropdown.
On this page
Start here
Jump straight to the call you need.
Copies this entire reference as clean markdown so ChatGPT or Claude can write your integration for you.
Where to find the API docs
The API docs live inside each subaccount. Create a custom location first, then open the API docs tab at the top of the portal to choose that location and edit its settings.
Where to find the API docs
Start at the agency level, create a custom location, then open the API docs tab to manage each subaccount.
1. Agency level
Log into Sendify Direct as an agency owner or admin.
2. Sub accounts
Open the Sub accounts section from the top navigation.
3. Custom location
Create a location, fill out the details, and save it.
4. API docs tab
Open API docs at the top of the portal.
5. Choose subaccount
Pick the custom location you just created.
6. Edit settings
Set webhook URL, timezone, and custom headers.
Pick your subaccount in the key dialog, copy the sk_ key once, and use it as the x-api-key header on every call.
The two clicks that matter
- 1In the Sendify portal, open Agency View, then Subaccounts, and click API Docs at the top.
- 2Choose your subaccount in the picker. You can keep several custom locations and switch between them here.
- 3Copy the sk_ key for that subaccount. It becomes the x-api-key header on every request.
Your API key
Every request needs your key in the header. The key is tied to a single subaccount, so it tells Sendify which account the contact or conversation belongs to. Keep it private and rotate it from the portal if it ever leaks.
Header on every request
x-api-key: sk_your_key_hereBase URL
https://d3vzg2unix0bsb.cloudfront.netCall order
Contacts come first, conversations second. The contact push is what gives you thecontactIdthat every AI call needs.
The standard call order
Every integration follows the same four steps. Skipping step two is the most common mistake.
1. Authenticate
Send x-api-key on every request. The key decides which subaccount you are writing to.
2. Push the contact
Create the contact and keep the contactId that comes back in the response.
3. Start the conversation
The AI sends the opening message to that contactId.
4. AI takes over
Replies, qualification, follow ups, and booking all run without another call.
Where contactId comes from
Your CRM sends a lead
Phone number is the one required field.
Sendify returns contactId
Save it on the record in your CRM right away.
Every AI call uses it
Start, stop, resume, and inbound messages all key off contactId.
Tags decide which AI agent answers
leadNew lead setterreactivationDatabase reactivation agentno-tagNo agent matches, conversation cannot startManaging contacts
/webhooks/contactsAdd one contact, or update an existing one
Create a single contact, or update an existing one by passing their contactId. Use this when leads arrive one at a time, or when you need to change a contact's details.
| Field | Required | Type | Description |
|---|---|---|---|
| phone | Optional | string | Required when creating a new contact. E.164 format recommended. Must be unique within your subaccount. |
| contactId | Optional | string | Pass the contactId to update an existing contact instead of creating a new one. All other fields you provide overwrite the current values. |
| firstName | Optional | string | Contact's first name. |
| lastName | Optional | string | Contact's last name. |
| Optional | string | Email address. Must be unique within your subaccount if provided. | |
| tags | Optional | string[] | Tags controlling which AI agent handles this contact. On update, tags replace the existing list entirely. |
| customFields | Optional | object | Custom field values keyed by field key. On update, customFields shallow merges into existing values. Keys you do not send are preserved, and setting a key to null removes that value. |
Request
curl -X POST https://d3vzg2unix0bsb.cloudfront.net/webhooks/contacts \
-H "Content-Type: application/json" \
-H "x-api-key: sk_your_key_here" \
-d '{"phone":"+15551234567","firstName":"Jane","lastName":"Doe","email":"jane@example.com","tags":["lead"],"customFields":{"lead_source":"inbound_call"}}'Response
{
"status": "success",
"message": "Contact created",
"data": {
"contactId": "ctc_abc123",
"status": "created",
"contact": {
"id": "ctc_abc123",
"phone": "+15551234567",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"tags": ["lead"],
"customFields": { "lead_source": "inbound_call" },
"createdAt": "2026-05-13T10:00:00.000Z"
}
}
}AI conversations and messaging
Stop and start are a switch, not a message
Stop
AI stops replying and stops follow ups for that one contact. Nothing is sent.
Start
AI resumes handling the conversation. Still nothing is sent.
Start conversation
This is the only call that actually sends an opening message.
/webhook/ai/startConversationTell the AI to send its first message to a contact
Triggers the AI agent to reach out to a contact with its first message. The AI picks up from there, so you do not need to do anything else for the conversation to continue.
| Field | Required | Type | Description |
|---|---|---|---|
| contactId | Required | string | The contact you want the AI to message. You get this from the create contact response. |
| userId | Optional | string | The Sendify user to assign this conversation to. Defaults to the owner of the API key. |
| message | Optional | string | Override the AI agent's default opening message for this contact only. Leave blank to use the agent's configured message. |
Request
curl -X POST https://d3vzg2unix0bsb.cloudfront.net/webhook/ai/startConversation \
-H "Content-Type: application/json" \
-H "x-api-key: sk_your_key_here" \
-d '{"contactId":"ctc_abc123","userId":"usr_xyz","message":"Hi Jane, thanks for signing up!"}'Response
{
"status": "success",
"message": "Initial messages processed successfully",
"data": {
"success": true,
"total_contacts": 1,
"processed_contacts": 1,
"failed_contacts": 0,
"results": [
{
"contact_id": "ctc_abc123",
"status": "success",
"conversation_id": "conv_xyz789"
}
]
}
}/webhook/ai/stopKill switch: the AI stops replying and following up with this contact
Turns the AI off for a single contact. It no longer replies to their messages or sends follow ups until you start it again. Use this when a human takes over, the contact opts out of AI handling, or your CRM flags them as do not engage.
| Field | Required | Type | Description |
|---|---|---|---|
| contactId | Required | string | The contact to stop the AI for. You get this from the create contact response. |
Request
curl -X POST https://d3vzg2unix0bsb.cloudfront.net/webhook/ai/stop \
-H "Content-Type: application/json" \
-H "x-api-key: sk_your_key_here" \
-d '{"contactId":"ctc_abc123"}'Response
{
"status": "success",
"message": "AI stopped for contact",
"data": {
"contactId": "ctc_abc123",
"isPaused": true,
"status": "disabled",
"aiSync": "synced"
}
}/webhook/ai/startTurn the AI back on for a contact you previously stopped
Re-enables the AI for a single contact after a stop. The AI resumes replying and running follow up sequences. It does not send any message by itself. To have the AI reach out first, use Start an AI conversation.
| Field | Required | Type | Description |
|---|---|---|---|
| contactId | Required | string | The contact to re-enable the AI for. You get this from the create contact response. |
Request
curl -X POST https://d3vzg2unix0bsb.cloudfront.net/webhook/ai/start \
-H "Content-Type: application/json" \
-H "x-api-key: sk_your_key_here" \
-d '{"contactId":"ctc_abc123"}'Response
{
"status": "success",
"message": "AI started for contact",
"data": {
"contactId": "ctc_abc123",
"isPaused": false,
"status": "enabled",
"aiSync": "synced"
}
}/webhooks/messages/inboundYour CRM delivers a message directly to a contact via Sendify
Use this when a CRM user wants to send a message to a contact through Sendify. The message is delivered over the channel you specify. No AI is triggered, this is a direct send on behalf of your system.
| Field | Required | Type | Description |
|---|---|---|---|
| phone | Required | string | The contact's phone number. Sendify uses this to find their contact record. |
| message | Optional | string | The text of the message to send. Either message or attachments must be present. |
| attachments | Optional | string[] | URLs of any media attachments, images or files, to include in the message. |
| contactId | Optional | string | If you already know the Sendify contactId, provide it to speed up the lookup. |
| channel | Optional | string | The messaging channel: SMS, IMESSAGE, MMS, WHATSAPP, or RCS. Defaults to SMS. |
| externalMessageId | Optional | string | A unique ID from your system. If you send the same ID twice, the second call is ignored, which prevents duplicate messages on retries. |
Request
curl -X POST https://d3vzg2unix0bsb.cloudfront.net/webhooks/messages/inbound \
-H "Content-Type: application/json" \
-H "x-api-key: sk_your_key_here" \
-d '{"phone":"+15551234567","message":"Hi Jane, just following up!","contactId":"ctc_abc123","externalMessageId":"msg_001","channel":"IMESSAGE"}'Response
{
"status": "success",
"message": "Message accepted",
"data": {
"messageId": "5917",
"contactId": "ctc_abc123"
}
}Outbound webhook events
These are not endpoints you call. Sendify posts them to the outbound webhook URL configured on your subaccount so your CRM stays in sync. Every payload carries a type field naming the event. Message events are flat. Set your webhook to POST. The other events wrap their details in a data object alongside a common envelope of type, subaccountId, contactId, and occurredAt.
Outbound events run the other direction
You do not call these. Sendify posts them to the webhook URL on your subaccount, and your endpoint answers with any 2xx within 10 seconds.
Something happens
A reply lands, the AI sends a message, a contact changes, or a booking is created.
Sendify posts to you
One JSON payload with a type field naming the event.
Your CRM updates
Answer 2xx in 10 seconds. Failures retry with backoff for about two days.
message.inbound / message.outboundFlat payload. Fields sit at the top level.
contact.updatedWrapped in data with an old to new diff.
appointment.bookedWrapped in data with the booking details.
message.inbound / message.outboundContact replies and AI-sent messages
Sent for the two message events: when a contact sends a reply, type message.inbound with direction inbound, and when the AI sends a message to a contact, type message.outbound with direction outbound. Use the type or direction field to label each message in your conversation log.
| Field | Required | Type | Description |
|---|---|---|---|
| type | Optional | string | The event type: message.inbound for a contact reply, message.outbound for an AI message. |
| event | Optional | string | Alias of type, same value. |
| subaccountId | Optional | string | The Sendify subaccount the message belongs to. |
| contactId | Optional | string | The Sendify contact ID. |
| contact | Optional | object | null | Full contact record for the recipient or sender. Null if the contact cannot be resolved. |
| contact.id | Optional | string | The contact ID, mirrors contactId. |
| contact.firstName | Optional | string | null | Contact's first name. |
| contact.lastName | Optional | string | null | Contact's last name. |
| contact.email | Optional | string | null | Contact's email address. |
| contact.phone | Optional | string | null | Contact's phone number. |
| contact.tags | Optional | string[] | Tags attached to the contact, used for AI routing. |
| contact.timezone | Optional | string | null | IANA timezone for the contact, if known. |
| contact.customFields | Optional | object | Custom field values keyed by field key. Empty object if none are set. |
| phone | Optional | string | The contact's phone number on this message. |
| message | Optional | string | The message text. |
| attachments | Optional | string[] | null | Any media files included in the message, or null. |
| channel | Optional | string | The messaging channel, for example SMS or IMESSAGE. |
| direction | Optional | string | inbound when the contact sent it, outbound when the AI sent it. |
| messageId | Optional | string | Sendify's internal ID for this message. |
| timestamp | Optional | string | ISO 8601 timestamp of when the message was sent. |
Your endpoint should respond with 2xx
{
"ok": true
}contact.updatedA contact's fields, custom fields, or tags changed
Fires whenever a contact changes: a manual edit, an AI automation writing a field, a tag added or removed, or a lead routing rule. One bundled event per change, carrying a structured diff of old to new so you can apply exactly what changed without re-fetching the contact.
| Field | Required | Type | Description |
|---|---|---|---|
| type | Optional | string | Always contact.updated. |
| event | Optional | string | Alias of type. |
| agencyId | Optional | string | The Sendify agency. |
| subaccountId | Optional | string | The Sendify subaccount. |
| contactId | Optional | string | The contact that changed. |
| occurredAt | Optional | string | ISO 8601 timestamp of the change. |
| data.changed | Optional | string[] | Which areas changed: standard field names plus customFields and tags. |
| data.standardFields | Optional | object | Changed standard fields keyed by name (firstName, lastName, email, phone, timezone), each with old and new. |
| data.customFields | Optional | object | Changed custom fields keyed by field key, each with old and new. |
| data.tagsAdded | Optional | string[] | Tags added in this change, empty if none. |
| data.tagsRemoved | Optional | string[] | Tags removed in this change, empty if none. |
| data.contact | Optional | object | Full current snapshot of the contact: firstName, lastName, email, phone, tags, timezone, customFields. |
Your endpoint should respond with 2xx
{
"ok": true
}appointment.bookedA booking was created for a contact
Fires when an appointment is booked for a contact, whether by the AI, an automation, or directly.
| Field | Required | Type | Description |
|---|---|---|---|
| type | Optional | string | Always appointment.booked. |
| agencyId | Optional | string | The Sendify agency. |
| subaccountId | Optional | string | The Sendify subaccount. |
| contactId | Optional | string | The contact the appointment is for. |
| occurredAt | Optional | string | ISO 8601 timestamp of the booking. |
| data.appointmentId | Optional | string | The booking's ID in the calendar provider. |
| data.title | Optional | string | null | Appointment title. |
| data.startTime | Optional | string | ISO 8601 start time. |
| data.endTime | Optional | string | ISO 8601 end time. |
| data.timezone | Optional | string | null | Timezone of the appointment. |
| data.provider | Optional | string | Calendar provider: google or ghl. |
| data.calendarId | Optional | string | The calendar the appointment was booked on. |
Your endpoint should respond with 2xx
{
"ok": true
}agent_status.changedThe AI was turned on, off, or set to no follow-ups for a contact
Fires when a contact's AI agent status changes, from the dashboard toggle, an automation, or when the AI reaches its goal and stops its own follow-ups.
| Field | Required | Type | Description |
|---|---|---|---|
| type | Optional | string | Always agent_status.changed. |
| agencyId | Optional | string | The Sendify agency. |
| subaccountId | Optional | string | The Sendify subaccount. |
| contactId | Optional | string | The contact whose status changed. |
| occurredAt | Optional | string | ISO 8601 timestamp of the change. |
| data.status | Optional | string | The new status: active (AI on), disabled (AI off), or followups-disabled (replies on, no proactive follow-ups). |
Your endpoint should respond with 2xx
{
"ok": true
}automation.completedAn AI automation or workflow finished running for a contact
Fires once when an automation workflow finishes for a contact, so your CRM can track that the automation ran.
| Field | Required | Type | Description |
|---|---|---|---|
| type | Optional | string | Always automation.completed. |
| subaccountId | Optional | string | The Sendify subaccount. |
| contactId | Optional | string | The contact the automation ran for. |
| occurredAt | Optional | string | ISO 8601 timestamp of completion. |
| data.executionId | Optional | string | Unique ID for this workflow run. Use it to de-duplicate. |
| data.workflowId | Optional | string | The workflow that ran. |
| data.status | Optional | string | completed if it finished, failed if a step errored. |
| data.trigger | Optional | string | What triggered the run. |
Your endpoint should respond with 2xx
{
"ok": true
}summary.generatedA Generate Summary automation produced a conversation summary
Fires when a Generate Summary automation node produces a summary of the conversation. The summary is also written to a contact field.
| Field | Required | Type | Description |
|---|---|---|---|
| type | Optional | string | Always summary.generated. |
| subaccountId | Optional | string | The Sendify subaccount. |
| contactId | Optional | string | The contact the summary is for. |
| occurredAt | Optional | string | ISO 8601 timestamp. |
| data.summary | Optional | string | The generated summary text. |
| data.targetField | Optional | string | The contact field the summary was written to. |
Your endpoint should respond with 2xx
{
"ok": true
}Setting this up in the portal
Everything above is switched on from Subaccounts in the Sendify portal. Two dialogs cover it: the key dialog and the custom location configuration.
Webhook API key
Subaccounts, then the key icon on the row. The key starts with sk_ and is shown once. Pass it as the x-api-key header on every call. Regenerate or revoke from the same dialog, and remember that regenerating breaks any integration still using the old key.
Custom location configuration
Subaccounts, then the gear icon on a custom location. Set the outbound webhook URL Sendify posts AI replies and events to, the timezone used for message times and business hours, and up to three custom headers sent on every outbound webhook so your receiver can verify the request.
Create the location once, then reopen it from the gear icon any time to change the webhook URL, timezone, or headers.
Create it once, edit it any time
- 1Create the custom location from Subaccounts. Fill in the name, address, email, phone, outbound webhook URL, timezone, and optional custom headers.
- 2Reopen it later from the gear icon on the location row to update the webhook URL, timezone, or headers.
- 3Changes apply to every outbound webhook Sendify posts for that subaccount.
| Field | Required | Type | Description |
|---|---|---|---|
| Location Name | Required | text | Label for the custom location inside Sendify. |
| Address | Required | text | Business address on the location. |
| Required | text | Contact email on the location. | |
| Phone Number | Required | text | E.164 format, for example 15672011207. |
| Outbound Webhook URL | Required | url | Where Sendify POSTs AI replies and every outbound event. |
| Timezone | Required | select | Used for displaying message times and business-hours scheduling. |
| Custom Headers | Optional | up to 3 | For example Authorization: Bearer ... Sent on every outbound webhook. |
FAQ
Where do I find my API key?
In the Sendify portal under Agency View, open API docs and pick the subaccount. The key starts with sk_ and belongs to that one subaccount, so it decides where your contacts and conversations land.
Do I need to send the key on every request?
Yes. Every request needs the header x-api-key with your sk_ key. Without it the request is rejected.
Why will the AI not start a conversation?
Two reasons cover almost every case. The contact has no tag that matches an agent routing rule, or you never pushed the contact first so you do not have a valid contactId.
Does stopping the AI send anything to the lead?
No. Stop and start only flip the AI on or off for that contact. Only Start an AI conversation sends a message.
How do I send a message without involving the AI?
Use the inbound messages endpoint. It delivers your text straight to the contact on the channel you pick and never triggers the AI. Pass externalMessageId so a retry cannot send the same message twice.
Do I have to call the outbound webhook events?
No. Sendify posts those to the webhook URL on your subaccount. Your endpoint just answers with any 2xx status within 10 seconds. Failed deliveries retry with backoff for about two days.
Where do I set the outbound webhook URL and custom headers?
In the portal under Subaccounts, open the gear icon on a custom location. You set the outbound webhook URL, the timezone used for message times and business hours, and up to three custom headers that are sent on every outbound webhook so your receiver can verify the request.
Can I call stop twice?
Yes. Stopping a contact that is already stopped returns the same result, so it is safe to retry.
Related articles
Still need a hand?
If your volume is going to run past the daily or hourly limits, or something is not behaving the way these docs describe, talk to our team.