Phone API
REST endpoints for phone in SKUFlow: 7 operations with parameters, request and response fields, permissions and curl examples.
Base URL: https://app.skuflow.ai/api. Every call needs Authorization: Bearer skf_… (Authentication); {organizationId} ($ORGANIZATION_ID in the examples) is your business's id.
| Operation | Endpoint | Risk |
|---|---|---|
| Phone settings | GET /organizations/{organizationId}/phone/settings | read |
| Change phone settings | PATCH /organizations/{organizationId}/phone/settings | write |
| Connect voice | POST /organizations/{organizationId}/phone/connect | sensitive |
| Browser phone token | POST /organizations/{organizationId}/phone/softphone-token | write |
| Calls ringing for me | GET /organizations/{organizationId}/phone/live | read |
| Simulate an inbound call (fake mode) | POST /organizations/{organizationId}/phone/simulate | write |
| Run phone work now | POST /organizations/{organizationId}/phone/run | write |
Phone settings
GET /organizations/{organizationId}/phone/settings
The phone system's settings: on/off, the mode (twilio, or fake = simulated calls), business hours in the business's timezone, after-hours voicemail or forwarding, ring timeout and the default ring group, voicemail greeting, call recording with its consent announcement and retention, transcription and AI summaries, missed-call follow-ups and text-back, and the webhook URLs Twilio calls. Read-only.
| Permission | phone.manage (Owner, Admin) |
| Risk | read, OAuth scope skuflow.read. Read: reads only. |
| MCP tool | phone_settings (toolset marketing) |
Example
curl "https://app.skuflow.ai/api/organizations/$ORGANIZATION_ID/phone/settings" \
-H "Authorization: Bearer $SKUFLOW_API_KEY"Response
200: the answer's fields
| Field | Type | Description |
|---|---|---|
mode | enum | fake = simulated calls (development, tests). One of fake, twilio, off. |
enabled | boolean | |
twilioSource | enum | Managed: the business's SKUFlow Twilio sub-account (billed at Twilio's cost + 20%); OWN: its own Twilio account connected in Settings → Messaging. One of MANAGED, OWN. |
managedAccount | object or null | The SKUFlow sub-account (MANAGED, once made). |
managedAccount.accountSidHint | string | |
managedAccount.status | enum | One of ACTIVE, SUSPENDED, CLOSED. |
managedAccount.fake | boolean | |
managedAccount.suspendReason | string or null | |
managedAccount.closeAfter | string (date-time) or null | |
accountConnected | boolean | The Twilio account calls run on is ready: the SKUFlow sub-account (active), or the business's own account connected for texts. |
browserCalling | boolean | The browser phone's API key and TwiML App exist. |
businessHours | object | |
afterHoursAction | enum | One of VOICEMAIL, FORWARD. |
afterHoursForwardNumber | string or null | |
ringTimeoutSeconds | integer | |
defaultRingGroupId | string or null | |
voicemailGreeting | string or null | |
recordCalls | boolean | |
announceRecording | boolean | |
recordingAnnouncement | string or null | |
businessState | string or null | |
recordingRetentionDays | integer | |
transcribeCalls | boolean | |
aiSummaries | boolean | |
missedCallFollowUp | boolean | |
missedCallTextBack | boolean | |
missedCallTextBody | string or null | |
webhookUrls | object or null | Where Twilio sends calls (set on bought or assigned numbers automatically). |
webhookUrls.voice | string | |
webhookUrls.status | string | |
webhookUrls.browser | string | |
timeZone | string | |
updatedAt | string (date-time) |
Change phone settings
PATCH /organizations/{organizationId}/phone/settings
Change how calls are handled from the next call on: business hours, after hours, ring timeout, default ring group, voicemail greeting, recording (announced by default; two-party-consent states are always announced), retention, transcription, AI summaries (metered against AI credits), missed-call follow-ups and the missed-call text (sent only to known customers whose number is OK to text). enabled: false stops answering calls.
| Permission | phone.manage (Owner, Admin) |
| Risk | write, OAuth scope skuflow.write. Write: creates or changes records inside the business; nothing leaves it. |
| MCP tool | phone_updateSettings (toolset marketing) |
| Plan | Needs the Phone system add-on. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | No | |
businessHours | object | No | Opening hours per weekday, e.g. { "mon": [{ "start": "08:00", "end": "17:00" }] }. |
businessHours.sun | array of object | No | Up to 4 items. |
businessHours.sun[].start | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.sun[].end | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.mon | array of object | No | Up to 4 items. |
businessHours.mon[].start | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.mon[].end | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.tue | array of object | No | Up to 4 items. |
businessHours.tue[].start | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.tue[].end | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.wed | array of object | No | Up to 4 items. |
businessHours.wed[].start | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.wed[].end | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.thu | array of object | No | Up to 4 items. |
businessHours.thu[].start | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.thu[].end | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.fri | array of object | No | Up to 4 items. |
businessHours.fri[].start | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.fri[].end | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.sat | array of object | No | Up to 4 items. |
businessHours.sat[].start | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
businessHours.sat[].end | string | Yes | Hh: MM, 24-hour, in the business's timezone. |
afterHoursAction | enum | No | One of VOICEMAIL, FORWARD. |
afterHoursForwardNumber | string or null | No | A phone number; +E.164 is safest (+13125550100). 10 digits are read as US. Up to 32 characters. |
ringTimeoutSeconds | integer | No | 5–60. |
defaultRingGroupId | string or null | No | Rings when a number has no group of its own. |
voicemailGreeting | string or null | No | Up to 500 characters. |
recordCalls | boolean | No | |
announceRecording | boolean | No | Announce recording (default on). Off is honoured only when both parties are known to be in one-party-consent states. |
recordingAnnouncement | string or null | No | Up to 500 characters. |
businessState | string or null | No | The business's US state for the recording consent rule. |
recordingRetentionDays | integer | No | 1–3650. |
transcribeCalls | boolean | No | |
aiSummaries | boolean | No | |
missedCallFollowUp | boolean | No | |
missedCallTextBack | boolean | No | Text a known customer back after a missed call (needs OK to text). |
missedCallTextBody | string or null | No | Up to 320 characters. |
Example
curl -X PATCH "https://app.skuflow.ai/api/organizations/$ORGANIZATION_ID/phone/settings" \
-H "Authorization: Bearer $SKUFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled":true,"businessHours":{"sun":[{"start":"09:00","end":"09:00"}],"mon":[{"start":"09:00","end":"09:00"}],"tue":[{"start":"09:00","end":"09:00"}]},"afterHoursAction":"VOICEMAIL"}'Response
200: the answer's fields
| Field | Type | Description |
|---|---|---|
mode | enum | fake = simulated calls (development, tests). One of fake, twilio, off. |
enabled | boolean | |
twilioSource | enum | Managed: the business's SKUFlow Twilio sub-account (billed at Twilio's cost + 20%); OWN: its own Twilio account connected in Settings → Messaging. One of MANAGED, OWN. |
managedAccount | object or null | The SKUFlow sub-account (MANAGED, once made). |
managedAccount.accountSidHint | string | |
managedAccount.status | enum | One of ACTIVE, SUSPENDED, CLOSED. |
managedAccount.fake | boolean | |
managedAccount.suspendReason | string or null | |
managedAccount.closeAfter | string (date-time) or null | |
accountConnected | boolean | The Twilio account calls run on is ready: the SKUFlow sub-account (active), or the business's own account connected for texts. |
browserCalling | boolean | The browser phone's API key and TwiML App exist. |
businessHours | object | |
afterHoursAction | enum | One of VOICEMAIL, FORWARD. |
afterHoursForwardNumber | string or null | |
ringTimeoutSeconds | integer | |
defaultRingGroupId | string or null | |
voicemailGreeting | string or null | |
recordCalls | boolean | |
announceRecording | boolean | |
recordingAnnouncement | string or null | |
businessState | string or null | |
recordingRetentionDays | integer | |
transcribeCalls | boolean | |
aiSummaries | boolean | |
missedCallFollowUp | boolean | |
missedCallTextBack | boolean | |
missedCallTextBody | string or null | |
webhookUrls | object or null | Where Twilio sends calls (set on bought or assigned numbers automatically). |
webhookUrls.voice | string | |
webhookUrls.status | string | |
webhookUrls.browser | string | |
timeZone | string | |
updatedAt | string (date-time) |
Connect voice
POST /organizations/{organizationId}/phone/connect
Turn the phone system on. Makes the business's SKUFlow Twilio sub-account the first time (MANAGED, the default; usage billed at Twilio's cost + 20%), then creates an API key and a TwiML App in that account (or in the business's own Twilio account when it chose OWN) for the browser phone; safe to repeat (the App's URLs are refreshed). In fake mode it records simulated ids. Call GET /phone/settings first to see the mode.
| Permission | phone.manage (Owner, Admin) |
| Risk | sensitive, OAuth scope skuflow.sensitive. Sensitive: moves money, deletes, sends or shows something to a customer, or grants or revokes access. |
| MCP tool | phone_connect (toolset marketing) |
| Plan | Needs the Phone system add-on. |
| Approval | Over MCP, this tool asks for your OK first (in the AI app when it supports it, otherwise in SKUFlow). See Approvals. |
Example
curl -X POST "https://app.skuflow.ai/api/organizations/$ORGANIZATION_ID/phone/connect" \
-H "Authorization: Bearer $SKUFLOW_API_KEY"Response
200: the answer's fields
| Field | Type | Description |
|---|---|---|
mode | enum | fake = simulated calls (development, tests). One of fake, twilio, off. |
enabled | boolean | |
twilioSource | enum | Managed: the business's SKUFlow Twilio sub-account (billed at Twilio's cost + 20%); OWN: its own Twilio account connected in Settings → Messaging. One of MANAGED, OWN. |
managedAccount | object or null | The SKUFlow sub-account (MANAGED, once made). |
managedAccount.accountSidHint | string | |
managedAccount.status | enum | One of ACTIVE, SUSPENDED, CLOSED. |
managedAccount.fake | boolean | |
managedAccount.suspendReason | string or null | |
managedAccount.closeAfter | string (date-time) or null | |
accountConnected | boolean | The Twilio account calls run on is ready: the SKUFlow sub-account (active), or the business's own account connected for texts. |
browserCalling | boolean | The browser phone's API key and TwiML App exist. |
businessHours | object | |
afterHoursAction | enum | One of VOICEMAIL, FORWARD. |
afterHoursForwardNumber | string or null | |
ringTimeoutSeconds | integer | |
defaultRingGroupId | string or null | |
voicemailGreeting | string or null | |
recordCalls | boolean | |
announceRecording | boolean | |
recordingAnnouncement | string or null | |
businessState | string or null | |
recordingRetentionDays | integer | |
transcribeCalls | boolean | |
aiSummaries | boolean | |
missedCallFollowUp | boolean | |
missedCallTextBack | boolean | |
missedCallTextBody | string or null | |
webhookUrls | object or null | Where Twilio sends calls (set on bought or assigned numbers automatically). |
webhookUrls.voice | string | |
webhookUrls.status | string | |
webhookUrls.browser | string | |
timeZone | string | |
updatedAt | string (date-time) |
Browser phone token
POST /organizations/{organizationId}/phone/softphone-token
What the browser phone needs for the signed-in person: mode TWILIO with a one-hour Twilio Voice access token (identity derived from the user; receives the ring group's calls, places calls through the business number), FAKE (simulated calls), OFF (phone system off) or UNAVAILABLE (connect voice first). Nothing else changes.
| Permission | phone.calls (Owner, Admin, Staff) |
| Risk | write, OAuth scope skuflow.write. Write: creates or changes records inside the business; nothing leaves it. |
| MCP tool | phone_softphone (toolset marketing) |
| Plan | Needs the Phone system add-on. |
Example
curl -X POST "https://app.skuflow.ai/api/organizations/$ORGANIZATION_ID/phone/softphone-token" \
-H "Authorization: Bearer $SKUFLOW_API_KEY"Response
200: the answer's fields
| Field | Type | Description |
|---|---|---|
mode | enum | One of TWILIO, FAKE, OFF, UNAVAILABLE. |
identity | string | |
token | string or null | |
expiresAt | string (date-time) or null |
Calls ringing for me
GET /organizations/{organizationId}/phone/live
Calls ringing for the signed-in person's ring groups and calls they are on: what the browser phone and the screen pop show. Poll every few seconds. Read-only (simulated calls nobody answered are ended as Twilio would).
| Permission | phone.calls (Owner, Admin, Staff) |
| Risk | read, OAuth scope skuflow.read. Read: reads only. |
| MCP tool | phone_live (toolset marketing) |
Example
curl "https://app.skuflow.ai/api/organizations/$ORGANIZATION_ID/phone/live" \
-H "Authorization: Bearer $SKUFLOW_API_KEY"Response
200: the answer's fields
| Field | Type | Description |
|---|---|---|
items | array of object | |
items[].id | string | |
items[].direction | enum | One of INBOUND, OUTBOUND. |
items[].status | enum | One of RINGING, IN_PROGRESS, COMPLETED, MISSED, VOICEMAIL, FAILED. |
items[].fake | boolean | A simulated call (SKUFLOW_VOICE=fake). |
items[].fromNumber | string | |
items[].toNumber | string | |
items[].otherNumber | string | The customer's side of the call. |
items[].phoneNumber | object or null | |
items[].phoneNumber.id | string | |
items[].phoneNumber.label | string | |
items[].phoneNumber.e164 | string | |
items[].source | enum or null | The tracked source of the number called; null = untracked. One of MAIN, WEBSITE, GOOGLE_BUSINESS, CAMPAIGN, QR_CODE, BUSINESS_UNIT, PRINT, OTHER. |
items[].campaign | object or null | |
items[].campaign.id | string | |
items[].campaign.name | string | |
items[].businessUnit | object or null | |
items[].businessUnit.id | string | |
items[].businessUnit.name | string | |
items[].customer | object or null | |
items[].customer.id | string | |
items[].customer.name | string | |
items[].matchedCustomerCount | integer | |
items[].user | object or null | Who answered (inbound) or placed it (outbound). |
items[].user.id | string | |
items[].user.name | string | |
items[].bridgeNumber | string or null | |
items[].ivrDigit | string or null | |
items[].afterHours | boolean | |
items[].startedAt | string (date-time) | |
items[].answeredAt | string (date-time) or null | |
items[].endedAt | string (date-time) or null | |
items[].durationSeconds | integer or null | Talk time. |
items[].billedMinutes | integer or null | |
items[].voicemail | boolean | |
items[].recording | object | |
items[].recording.status | enum | One of NONE, PENDING, DONE, FAILED, SKIPPED. |
items[].recording.announced | boolean | |
items[].recording.durationSeconds | integer or null | |
items[].recording.available | boolean | |
items[].recording.deletedAt | string (date-time) or null | |
items[].recording.error | string or null | |
items[].transcriptStatus | enum | One of NONE, PENDING, DONE, FAILED, SKIPPED. |
items[].transcript | string or null | Only for callers holding phone.recordings, on GET /phone/calls/{callId}. |
items[].summaryStatus | enum | One of NONE, PENDING, DONE, FAILED, SKIPPED. |
items[].summary | string or null | The AI call summary (Claude, from the transcript). |
items[].summaryError | string or null | |
items[].followUp | object | |
items[].followUp.status | enum | One of NONE, OPEN, DONE. |
items[].followUp.assignee | object or null | |
items[].followUp.doneAt | string (date-time) or null | |
items[].textBack | object or null | |
items[].textBack.status | string | |
items[].textBack.messageId | string or null | |
items[].request | object or null | |
items[].request.id | string | |
items[].request.identifier | string | |
items[].notes | string or null | |
items[].createdAt | string (date-time) | |
items[].updatedAt | string (date-time) |
Simulate an inbound call (fake mode)
POST /organizations/{organizationId}/phone/simulate
Only with simulated voice (SKUFLOW_VOICE=fake; 409 otherwise): an inbound call from from to one of the business's numbers, through the same steps as Twilio's webhooks (routing, caller match, menu, recording, transcript, AI summary, follow-up, text-back). RING leaves it ringing for the browser phone; ANSWERED, MISSED and VOICEMAIL play it through. now is test-only (refused in production).
| Permission | phone.manage (Owner, Admin) |
| Risk | write, OAuth scope skuflow.write. Write: creates or changes records inside the business; nothing leaves it. |
| MCP tool | phone_simulate (toolset marketing) |
| Plan | Needs the Phone system add-on. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | A phone number; +E.164 is safest (+13125550100). 10 digits are read as US. Up to 32 characters. |
phoneNumberId | string or null | No | The number called; default the main line. |
digit | string or null | No | |
outcome | enum | No | One of RING, ANSWERED, MISSED, VOICEMAIL. Default RING. |
answeredByUserId | string or null | No | |
talkSeconds | integer | No | 1–7200. |
transcript | string or null | No | |
callerState | string or null | No | |
now | string (date-time) | No |
Example
curl -X POST "https://app.skuflow.ai/api/organizations/$ORGANIZATION_ID/phone/simulate" \
-H "Authorization: Bearer $SKUFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from":"2026-11-03"}'Response
200: the answer's fields
| Field | Type | Description |
|---|---|---|
id | string | |
direction | enum | One of INBOUND, OUTBOUND. |
status | enum | One of RINGING, IN_PROGRESS, COMPLETED, MISSED, VOICEMAIL, FAILED. |
fake | boolean | A simulated call (SKUFLOW_VOICE=fake). |
fromNumber | string | |
toNumber | string | |
otherNumber | string | The customer's side of the call. |
phoneNumber | object or null | |
phoneNumber.id | string | |
phoneNumber.label | string | |
phoneNumber.e164 | string | |
source | enum or null | The tracked source of the number called; null = untracked. One of MAIN, WEBSITE, GOOGLE_BUSINESS, CAMPAIGN, QR_CODE, BUSINESS_UNIT, PRINT, OTHER. |
campaign | object or null | |
campaign.id | string | |
campaign.name | string | |
businessUnit | object or null | |
businessUnit.id | string | |
businessUnit.name | string | |
customer | object or null | |
customer.id | string | |
customer.name | string | |
matchedCustomerCount | integer | |
user | object or null | Who answered (inbound) or placed it (outbound). |
user.id | string | |
user.name | string | |
bridgeNumber | string or null | |
ivrDigit | string or null | |
afterHours | boolean | |
startedAt | string (date-time) | |
answeredAt | string (date-time) or null | |
endedAt | string (date-time) or null | |
durationSeconds | integer or null | Talk time. |
billedMinutes | integer or null | |
voicemail | boolean | |
recording | object | |
recording.status | enum | One of NONE, PENDING, DONE, FAILED, SKIPPED. |
recording.announced | boolean | |
recording.durationSeconds | integer or null | |
recording.available | boolean | |
recording.deletedAt | string (date-time) or null | |
recording.error | string or null | |
transcriptStatus | enum | One of NONE, PENDING, DONE, FAILED, SKIPPED. |
transcript | string or null | Only for callers holding phone.recordings, on GET /phone/calls/{callId}. |
summaryStatus | enum | One of NONE, PENDING, DONE, FAILED, SKIPPED. |
summary | string or null | The AI call summary (Claude, from the transcript). |
summaryError | string or null | |
followUp | object | |
followUp.status | enum | One of NONE, OPEN, DONE. |
followUp.assignee | object or null | |
followUp.assignee.id | string | |
followUp.assignee.name | string | |
followUp.doneAt | string (date-time) or null | |
textBack | object or null | |
textBack.status | string | |
textBack.messageId | string or null | |
request | object or null | |
request.id | string | |
request.identifier | string | |
notes | string or null | |
createdAt | string (date-time) | |
updatedAt | string (date-time) |
Run phone work now
POST /organizations/{organizationId}/phone/run
Run this business's pending phone work now instead of waiting for the worker: recording downloads into storage, AI summaries, simulated calls nobody answered, and recordings past retention. now is test-only (refused in production).
| Permission | phone.manage (Owner, Admin) |
| Risk | write, OAuth scope skuflow.write. Write: creates or changes records inside the business; nothing leaves it. |
| MCP tool | phone_run (toolset marketing) |
| Plan | Needs the Phone system add-on. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
now | string (date-time) | No |
Example
curl -X POST "https://app.skuflow.ai/api/organizations/$ORGANIZATION_ID/phone/run" \
-H "Authorization: Bearer $SKUFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"now":"2026-11-03T15:00:00Z"}'Response
200: the answer's fields
| Field | Type | Description |
|---|---|---|
processed | integer | |
purged | integer |