Download the PHP package sendly/sendly-php without Composer
On this page you can find all versions of the php package sendly/sendly-php. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package sendly-php
Sendly PHP SDK
Official PHP SDK for the Sendly SMS API.
Requirements
- PHP 8.1+
- Composer
Installation
Quick Start
Prerequisites for Live Messaging
Before sending live SMS messages, you need:
-
Business Verification - Complete verification in the Sendly dashboard
- International: Instant approval (just provide Sender ID)
- US/Canada: Requires carrier approval
-
Credits - Add credits to your account
- Test keys (
sk_test_*) work without credits (sandbox mode) - Live keys (
sk_live_*) require credits for each message
- Test keys (
- Live API Key - Generate after verification + credits
- Dashboard → API Keys → Create Live Key
Test vs Live Keys
| Key Type | Prefix | Credits Required | Verification Required | Use Case |
|---|---|---|---|---|
| Test | sk_test_v1_* |
No | No | Development, testing |
| Live | sk_live_v1_* |
Yes | Yes | Production messaging |
Note: You can start development immediately with a test key. Messages to sandbox test numbers are free and don't require verification.
Configuration
maxRetries (default 3) applies to connection failures, timeouts and 5xx
responses, with exponential backoff between attempts (1, 2, then 4 seconds).
A 4xx response throws straight away, with one exception: a 429
too_many_concurrent_verifications means too many first-time API key checks
were running at once from your address, so the request never ran. The client
waits its Retry-After (1 second) and sends it again, within the same
maxRetries. Every other 429 throws; see Rate Limits. Every
retry carries the same idempotency key.
timeout defaults to 30 seconds (the connect timeout is fixed at 10).
organization_id sends an X-Organization-Id header; it falls back to the
SENDLY_ORG_ID environment variable, and you can change it later with
$client->setOrganizationId('org_yyy'). The API ignores this header for API
keys: a key always acts in the workspace it was created in, so to work in
another workspace use a key created there.
Messages
Send an SMS
List Messages
A page holds at most 100 messages; a larger limit is capped at 100. A test
key lists only sandbox messages and a live key only live ones.
Get a Message
Scheduling Messages
Batch Messages
Iterate All Messages
each() requests page after page (up to 100 messages each, set with
batchSize) until it has yielded every matching message, so on a large
account it makes many requests; break out of the loop to stop early.
Group MMS
Send one MMS to 2-8 recipients (US/Canada only). Everyone shares a single
thread and replies fan out to all participants. Group messaging is an A2P
10DLC capability — the sending number must be an MMS-enabled, 10DLC-registered
number you own. Requires the group_mms feature (and enable_mms for media)
to be enabled for your account.
Billed per recipient at the MMS rate. Without the group_mms feature the API
answers 403 feature_disabled (SendlyException). A group message the
carrier refuses throws ValidationException (422 send_failed), and its
credits are refunded.
AI Message Enhancement
Rewrite a draft into a single, polished SMS segment (≤160 characters) and get a
short explanation of what changed. Pass messageType to steer the rewrite; with
no text it generates a suitable message for that type instead. At least one of
text or messageType is required. Requires the ai_classification feature;
when AI is unavailable the original text is returned with an empty explanation.
Idempotency
POSTs carry an automatically generated Idempotency-Key. The client keeps
that key for every retry it makes on its own (after a 5xx, a timeout, a
connection failure, or the busy key-check 429), so a retry of a request that
already reached the API returns the original result instead of sending and
charging again. Uploads also carry a key, but the API does not deduplicate
them, so a retried upload can store the file twice.
On the endpoints that deduplicate (sends, batch, group, schedule, conversation
replies, draft approval, verify, number purchase, credit transfers, enterprise
provisioning, WhatsApp signup and template creation, calls, and
RCS and short-code writes) the API records the answer under the key for a 2xx
and for every 4xx except 429, and repeating the request with the same key
within 24 hours returns that recorded answer. A 5xx or a 429 is never recorded, so a retry under the same
key runs the request again. Reusing one of your own keys with a different body
is refused with 422 idempotency_key_mismatch (ValidationException). Other
POSTs ignore the key, so a retried call there runs again.
Pass your own key (1-255 printable ASCII characters) when the guarantee needs
to outlive the process, such as a job queue that re-runs after a crash.
sendBatch() sends no automatic key, because the API already deduplicates
identical batches by their contents.
Numbers
Browse the countries Sendly provisions numbers in, search live availability, buy a number, list the numbers on your account, inspect one, make a number your default sender or keep a number scheduled for release, and release a number.
Short Links (URL Shortening)
Mint branded short links for a destination URL, list them with click analytics, and disable (kill) an individual link. Branded, owned-domain short links improve deliverability — carriers filter public shorteners — and give you click data.
Note: URL shortening is gated behind the
url_shortenerrollout flag and is not yet generally available; until the flag is enabled for your account the endpoints read as absent and calls throwNotFoundException(HTTP 404).
Connect a number you own to WhatsApp, set up its profile, photo, ice
breakers, commands and calling, create Meta-reviewed message templates, check
24-hour conversation windows, and send WhatsApp messages by passing
'channel' => 'whatsapp' to messages()->send().
Connecting a number costs a one-time $9.99 fee (no monthly fee). The first number
ends with a human step: the signup returns a connectUrl that a person must
open in a browser and log in with Facebook to link their WhatsApp Business
Account. More numbers can join that account without it: WhatsApp texts or
calls the number a 6-digit code, which you submit.
Free-form text and media only deliver inside an open 24-hour window (the
recipient messaged you in the last 24h); an approved template works anytime.
Note: The WhatsApp channel is being rolled out gradually and is not yet generally available. It is enabled per person: the user who owns the API key, not the workspace. While it is off, the
/api/v1/whatsapp/*management routes return 404not_found(thrown asNotFoundException), and a'channel' => 'whatsapp'send throwsSendlyException(HTTP 403,whatsapp_not_enabled).
Sends go through messages()->send() with 'channel' => 'whatsapp' and need
sms:send, not whatsapp:write. Reads (signup->get, templates, the window,
senders and sender profiles) need whatsapp:read and accept test keys.
Signup, template create/edit/delete and profile edits need whatsapp:write
and a live key (otherwise 403 whatsapp_requires_live_key). Sends need a
live key too. In a team workspace, connecting and profile edits need an owner
or admin (settings:write), and template writes need an owner, admin or
member (templates:write). A missing role returns 403
insufficient_permissions.
While WhatsApp connections are unavailable a Facebook signup answers 503
whatsapp_unavailable before charging anything, with retryAfter: 3600 in
the body and a Retry-After: 3600 header; the client retries it like any
5xx, then throws SendlyException. Only signup returns it; no send does. The
one-time $9.99 connection fee is charged to your card when you start
connecting a number and refunded automatically if the connection fails; it is
not refundable once the number is connected, and a later disconnect gets
nothing back. After 5 failed, charged signups in 24 hours it answers 429
whatsapp_signup_limit_reached, thrown as RateLimitException: try again the
next day. A send WhatsApp refuses throws ValidationException (422
whatsapp_send_failed, final; cached under the idempotency key and replayed
for 24 hours). A 503 whatsapp_send_failed means the message provably never
reached WhatsApp, so it was not sent and is safe to resend under the same
idempotency key; it is never cached, and the client retries it automatically,
up to 3 times under the same key, before throwing. Neither is charged. With
'fallbackToSms' => true, a 422 means the SMS couldn't go either; see
SMS fallback. A 409 whatsapp_send_unconfirmed (SendlyException) means the
outcome is unknown: the message was marked failed and refunded but may still
be delivered, so check before sending it again (it could arrive twice). It is
not retried automatically.
Adding a number with businessAccountId runs the same number checks, fee,
refund and daily limit as a Facebook connection. It answers 404
whatsapp_business_account_not_found when no connected account with that id
is in the workspace, 400 display_name_required when neither you nor the
account supplies a display name, and whatsapp_verification_start_failed
when WhatsApp won't send the code (422 refused, 503 unreachable; the signup
fails and the fee is refunded). The client never retries these three calls
after a 5xx or a failed connection; it throws at once instead: create() with
businessAccountId (a retry could start another signup with its own charge
and refund that counts toward the daily limit), verify() (every submission
uses up one of the five attempts) and uploadProfilePhoto(). resend() is
retried like any other call. verify() and resend() need the same scope,
key and role as signup. The profile photo, ice breakers and commands, and calling
follow the profile rules: getConversationalComponents() needs
whatsapp:read, and the changes need whatsapp:write, a live key and, in a
team workspace, an owner or admin. WhatsApp only allows calling once the
account may message at least 2,000 people a day and its display name is
approved (otherwise 422 whatsapp_calling_unavailable). Calls from WhatsApp
users are billed at the normal inbound rate, and the API has no endpoint for
placing WhatsApp calls.
Template pre-flight refusals are 400s: template_category_invalid (category
missing or not one of UTILITY, AUTHENTICATION and MARKETING; there is
no default), template_authentication_otp_button_required,
template_authentication_no_links (a link in the body or a URL button on an
authentication template) and template_header_variable_unsupported. On
create, 404 whatsapp_sender_not_connected is checked first. An update can't
change the category, and a marketing template without an opt-out button only
gets a warning.
Pricing: free-form text or media inside the 24-hour window costs 1 credit each for the first 1,000 per sending number per calendar month (UTC), then the destination's utility template price; countries without a listed price use the default utility price of 12 credits. Templates are priced by category and destination country; countries without a listed price use 33 (marketing), 12 (utility) and 12 (authentication) credits. A failed send gives its slot back.
SMS fallback
Pass 'fallbackToSms' => true and a WhatsApp message that can't be delivered
goes as an SMS from the same number instead.
- It falls back when the 24-hour window is closed (the SMS goes at once: free-form text and media go only within 24 hours of the customer's last message, templates any time), when WhatsApp refuses the message, or when WhatsApp reports it undeliverable later. Never after WhatsApp reports it delivered or read, and at most once per message.
- The number must already be able to send SMS to the recipient (a verified
toll-free number, or a local number on a 10DLC campaign, to a US or Canada
number), or the send is refused with 403
sms_fallback_unavailablebefore anything is sent. - Marketing and authentication templates, and media without a caption, can't
fall back: 422
sms_fallback_not_supported. A utility template falls back as its text header, body and footer with the variables filled in, and its URL and phone buttons aslabel: target; quick replies are dropped and listed incontentDropped. - Billing: the WhatsApp charge is refunded (a closed window is never charged)
and the SMS is billed as an SMS. A message that fell back at once comes
back with
status"failed",creditsUsed0 andfallback.status"sent"; the fallback'screditsUsedis what the SMS cost. - No SMS goes on a 409
whatsapp_send_unconfirmedor a 503whatsapp_send_failed. The status is"unavailable"when SMS rules stop it (the contact opted out, a promotional-looking text in the recipient's quiet hours, restricted content), with the reason in itserror, and"skipped"for a WhatsApp error an SMS can't fix. - There is no time-based fallback: a message WhatsApp accepted but hasn't
delivered, for example to a phone that is off, stays
"pending", and no SMS goes unless WhatsApp later reports a failure. messages()->get()andmessages()->list()return the same fallback (as$message->fallback, aWhatsAppSmsFallback) on the WhatsApp message, so check a pending one later. The SMS carries$message->fallbackOf, the WhatsApp message's id.
Guide: Send WhatsApp with an SMS Fallback.
RCS
Send branded rich messages — text with suggested replies and actions, or rich
cards with an image and buttons — through your workspace's RCS agent by
passing 'channel' => 'rcs' to messages()->send(). Delivery is
per-recipient: not every device or network supports RCS. Text sends fall back
to plain SMS automatically (billed as SMS) unless you disable the fallback;
rich cards have no SMS form and only deliver to RCS-capable recipients.
Note: The RCS channel is being rolled out gradually and is not yet generally available; until it is enabled for your account the endpoints read as absent and calls throw
NotFoundException(HTTP 404; the registration endpoints answerrcs_not_enabled). RCS sends and capability checks require a live API key. Registration reads need an API key with thercs:readscope and writes thercs:writescope.
Register a brand and agent
Registration is self-serve, from the RCS section of your dashboard or over the
API, and open to US businesses for now. Draft a brand (your business identity)
and an agent (the sender recipients see), then submit them. Sendly reviews the
registration first, then the carrier network verifies your business and
reviews the agent. Once the agent reaches testing you invite handsets, send
to them, file the campaign details, and request launch.
Logo, hero and call-to-action media must already be hosted at public
https:// URLs. File upload is dashboard-only.
Send over RCS
Voice Calls
Place a phone call that one of your workspace's AI agents handles, follow it
while it rings and runs, end it early, and fetch the recording afterwards.
Agents, which numbers take calls and how they answer, and emergency addresses
are set up from code with $client->voice or in the
dashboard under Calls; $client->voice->numbers->list() reports
voiceEnabled, voiceMode and the emergency address on each number so you
can pick one to call from.
Note: Calls are prepaid from your credit balance per started minute: 2 credits a minute outbound plus 8 credits a minute while an AI agent is on the call (10 credits a minute in total), US and Canada only, and unanswered calls cost nothing. The
fromnumber must be voice-enabled and have an emergency address registered. Voice is enabled workspace by workspace; until it is on for your account the endpoints throwNotFoundException(voice_not_enabled). Reads need thecalls:readscope, writescalls:writeand a live key.
| Method | Endpoint | Scope | Description |
|---|---|---|---|
calls()->create($params, $idempotencyKey = null) |
POST /calls |
calls:write |
Place a call handled by an AI agent. to and agentId required; from, context, metadata optional. |
calls()->list($options = []) |
GET /calls |
calls:read |
List calls. limit, offset, status, direction, kind, agentId, to, from. Returns data and pagination (total, limit, offset, hasMore). |
calls()->get($id) |
GET /calls/{id} |
calls:read |
One call, plus transcript on agent-handled calls. |
calls()->hangup($id, $idempotencyKey = null) |
POST /calls/{id}/hangup |
calls:write |
End a ringing or active call. |
calls()->recording($id) |
GET /calls/{id}/recording |
calls:read |
status (none, recording, ready, failed), url and expiresAt (set when ready), contentType (audio/ogg). |
CallStatus, CallDirection, CallKind, CallChannel, CallHandledBy,
CallBilling, CallRecordingStatus and CallErrorCode in Sendly\Resources
hold the string values the endpoints use. A call's channel says how the other
party reached it: phone, whatsapp or browser (more may be added). Calls
placed through the API are phone calls. A call's billing is metered while a phone call is
charged per minute, settled once it has ended, and unbilled for calls that
were never charged (browser-to-browser calls between teammates are free).
hangupClass says why a call ended: normal, caller_hung_up,
callee_hung_up and agent_agent_hangup are ordinary endings; ring_timeout,
callee_busy, callee_declined and caller_cancelled mean it never
connected; max_duration (60 minutes) and credits_exhausted mean the
platform cut it short.
The call.started, call.completed and call.recording.ready webhooks carry
the call at $event->object in snake_case (channel, handled_by,
duration_secs, credits_charged, hangup_class, recording_status,
billing, metadata).
Configure voice
Set up everything a call depends on from code: switch voice on for a number
and choose how it answers, register its emergency address, and create the AI
agents that talk. $client->voice uses the same calls:read and
calls:write scopes, and writes need a live key. In a team workspace,
changing a number or its emergency address needs a role that can change
settings, and managing agents needs a role that can manage API keys (each
agent holds its own scoped sending key); otherwise the API responds
403 forbidden. Address a number by its id or its E.164 phone number.
| Method | Endpoint | Scope | Description |
|---|---|---|---|
voice->numbers->list() |
GET /voice/numbers |
calls:read |
Active numbers under data, each with voiceEnabled, voiceMode, agentId, emergencyAddress and ratePerMinute. |
voice->numbers->get($number) |
GET /voice/numbers/{number} |
calls:read |
One number, by id or E.164 phone number. |
voice->numbers->update($number, $params) |
PATCH /voice/numbers/{number} |
calls:write |
voiceEnabled, voiceMode (none, ring_dashboard, agent), agentId (null clears it). |
voice->numbers->registerEmergencyAddress($number, $params, $idempotencyKey = null) |
POST /voice/numbers/{number}/emergency-address |
calls:write |
street, city, state, zip required; unit and country (US, the default, or CA) optional. |
voice->agents->list() |
GET /voice/agents |
calls:read |
Agents under data, with callsHandled and avgDurationSecs. |
voice->agents->create($params, $idempotencyKey = null) |
POST /voice/agents |
calls:write |
name required; enabled, voice, language, greeting, instructions, tools (sendSms, transferTo) optional. |
voice->agents->get($id) |
GET /voice/agents/{id} |
calls:read |
One agent. |
voice->agents->update($id, $params) |
PATCH /voice/agents/{id} |
calls:write |
Any subset of the create fields. |
voice->agents->delete($id) |
DELETE /voice/agents/{id} |
calls:write |
Delete an agent and revoke its sending key. Returns id, object and deleted. |
voice->voices->list() |
GET /voice/voices |
calls:read |
Voices (id, label, language) under data. |
Every resource is also reachable method-style: $client->voice()->numbers()->list().
A voiceMode on its own is enough: ring_dashboard or agent switches voice
on (and can be refused like any switch-on) and none switches it off. When
voiceEnabled is sent too it wins: false switches voice off, and true with
none rings the dashboard. A
workspace can have up to 20 agents, and an unknown voice id falls back to the
default voice. Agents can't transfer calls yet: with tools.transferTo set, a
caller who asks for a person is told the message will be passed on and the
agent takes their name and number.
Refusals: number_not_found and agent_not_found (404, NotFoundException);
invalid_request, invalid_voice_mode, agent_required, invalid_address
and e911_not_applicable (400, ValidationException); invalid_address
(422, ValidationException, with a corrected address or null under
suggested in getResponseBody()); agent_disabled (409, switch the agent
on first), agent_limit (409) and agent_in_use (409, the numbers the agent
still answers under numbers in getResponseBody()); voice_attach_failed
(503, try again); carrier_refused (503, try again, unless the message says
the number couldn't be found for emergency registration: contact support); and
voice_unavailable (503). The
409 and 503 refusals arrive as SendlyException with getCode() and
getApiErrorCode() set, and CallErrorCode holds every code.
Webhooks
Receiving webhook events
Webhooks::parseEvent() verifies the signature and returns a WebhookEvent.
$event->object is data.object exactly as it arrived, for every event type.
Keys are verbatim, nothing is defaulted, and a JSON null stays null:
$event->data is the message view of data.object. It is populated for
message.* events and is null for every other event type, because their
payload is not a message. Check it before reading through it:
Every field on $event->data is nullable and reflects what the payload
carried. A field the event did not send is null, never a stand-in such as
'', 1 or 'outbound'.
Typed views for other payloads come from objectAs(), which builds a class
through its static fromArray(array $data) when it has one and otherwise
through a constructor taking the array:
Other helpers: $event->get('key', $default) reads one key off
data.object ($default applies only when the key is absent, so a null the
server sent stays null), and WebhookEvent::isMessageEvent($type) tells you
whether an event type carries a message.
Handling a lifecycle event
Lifecycle events — rcs_*, whatsapp_*, call.*, brand.*, campaign.*,
assignment.*, number.*, port*, contact* — carry their own object at
data.object, not a message. Read them off $event->object; $event->data is
null for all of them. A complete endpoint:
Reaching through $event->data on one of these events is the mistake this release
makes visible: on 3.x it handed back '', 0, 1 or 'outbound' and raised
nothing, so $event->data->id on rcs_agent.live silently acted on an empty id.
$event->data is null there now.
Verify (OTP)
Send and check one-time codes, or hand the whole flow to a Sendly-hosted page. Request and response fields on this resource are snake_case.
Hosted verification sessions
Sendly hosts the phone-entry and code-entry pages; you get a one-time token back.
Templates
Reusable message bodies with {{variable}} placeholders, plus Sendly's presets.
Contacts & Lists
Campaigns
Send one message to a contact list, now or on a schedule. Every campaign is
sent as a marketing message, subject to quiet hours, and targets one contact
list. The segmentId and messageType options are deprecated: the API reads
neither.
Campaign status constants live on the class: Campaigns::STATUS_DRAFT,
STATUS_SCHEDULED, STATUS_SENDING, STATUS_COMPLETED (what a sent campaign
becomes), STATUS_FAILED and STATUS_CANCELLED. STATUS_SENT is never
returned, though as a list() filter it matches completed campaigns, and
STATUS_PAUSED is deprecated: campaigns cannot be paused.
Deprecated:
pause(),resume(),stats()andrecipients()have no route on the API and fail with a 404. Read the campaign withget()instead.
Conversations
Two-way threads, with the AI helpers built on them.
Labels, Drafts and Rules
Media
Upload an attachment and send it as MMS.
10DLC
Register your business so you can text from US local (10-digit) numbers. Brand,
campaign and assignment writes need a live key with the tendlc:write scope.
Short Codes
This SDK still has no short-code helper methods. Short codes are reachable over REST with the client's own request methods, which return the decoded response and throw the same exceptions as every other call:
Reads need an API key with the short_codes:read scope and writes
short_codes:write. Until short codes are enabled for your account these
endpoints read as absent and throw NotFoundException
(short_codes_not_enabled); a second application while one is open answers
409 short_code_application_exists.
A code's status moves requested → provisioning → parked → active
(cancelled ends it), and our review gate's reviewStatus moves draft →
awaiting_review → approved_for_filing → filed (rejected ends it). A code
stays parked until brand and content-provider registration finish. Subscribe
to the short_code.action_required, short_code.rejected, short_code.filed
and short_code.live webhooks to follow it without polling.
Business Upgrade
When a customer forms a new legal entity, reserve a new toll-free number under it and swap over on carrier approval — the current number keeps sending throughout the 1-2 week review.
Account & Credits
A live key needs a verified business and a credit balance: without them
createApiKey() gets a 403 verification_required (SendlyException) or a
402 credits_required (InsufficientCreditsException). Asking for a scope the
calling key lacks is refused with 403 insufficient_permissions; without
scopes the new key gets the calling key's scopes.
Rate Limits
Requests are counted per API key in a fixed 60-second window that starts with the first request and resets when it runs out:
| Key | Requests per minute |
|---|---|
Test (sk_test_v1_*) |
60 |
Live (sk_live_v1_*) |
600 |
| Enterprise master | 3000 |
Over the limit the API answers 429 rate_limit_exceeded and the SDK throws
RateLimitException; getRetryAfter() gives the seconds to wait, from the
Retry-After header or, when there is none, the body's retryAfter (as on
verify()->send()'s per-phone and daily limits). Responses also carry
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
The client waits out one 429 itself: too_many_concurrent_verifications
(too many first-time API key checks running at once from your address). The
request never ran, so it is sent again after its Retry-After, with the same
idempotency key, within maxRetries. Every other 429 throws at once. Branch on
getApiErrorCode():
Error Handling
Every exception above extends SendlyException, so a single
catch (SendlyException $e) catches them all.
Some ValidationExceptions are thrown before any request is sent: a missing
required argument, text over 1,600 characters (counted in characters, not
bytes), and an id of . or ... An id like that would otherwise be resolved
as a path segment and reach a different endpoint, so every method, and a path
passed to get(), post(), put(), patch(), delete() or
postMultipart(), refuses it.
WebhookSignatureException is the exception: it extends PHP's Exception
directly, so catching SendlyException will not catch it. Catch it by name
in your webhook handler.
Some refusals carry extra detail in getResponseBody() — a 409 agent_in_use
lists the numbers the agent still answers under numbers, a 422
invalid_address carries a corrected address (or null) under suggested, and
a 422 whatsapp_verification_code_invalid says how many tries are left under
attemptsRemaining.
Message Object
Message Status
| Status | Description |
|---|---|
queued |
Message is queued for delivery |
sent |
Message was sent to carrier |
delivered |
Message was delivered |
failed |
Message delivery failed |
bounced |
Carrier rejected the message |
retrying |
Delivery failed and is being retried |
read |
Recipient read the message (channels that report it) |
received |
Inbound message from a recipient |
Pricing Tiers
1 credit is $0.01. A message costs credits per segment, by destination country:
| Tier | Example countries | Credits per SMS |
|---|---|---|
| Domestic | US, CA | 2 |
| Tier 1 | GB, AU, PL, SE, BR | 8 |
| Tier 2 | FR, JP, IT, IN, ES | 12 |
| Tier 3 | DE, NL, MX, BE | 16 |
| Tier 4 | UA, VN, PA, GE | 24 |
| Tier 5 | IL, MY, PH, ID | 48 |
previewBatch() returns the exact per-country credits for a run before you
send it, and its byCountry breakdown names each country's tier.
Sandbox Testing
Use test API keys (sk_test_v1_xxx) with these test numbers:
| Number | Behavior |
|---|---|
| +15005550000 | Success (instant) |
| +15005550001 | Fails: invalid_number |
| +15005550002 | Fails: unroutable_destination |
| +15005550003 | Fails: queue_full |
| +15005550004 | Fails: rate_limit_exceeded |
| +15005550006 | Fails: carrier_violation |
Enterprise
The Enterprise API lets you programmatically manage workspaces, verification, credits, and API keys for multi-tenant platforms. It requires an enterprise master key — an ordinary live key (sk_live_v1_…) that has been marked as your organization's master key in the dashboard; what distinguishes it is the flag on the key, not the prefix. A non-master key is refused with 403 enterprise_required, and a master key whose enterprise account is inactive with 403 enterprise_inactive. Master keys also get the higher rate limit of 3,000 requests a minute.
Quick Provision
Create a fully configured workspace in a single call:
Three provisioning modes:
| Mode | Params | Description |
|---|---|---|
| Inherit | sourceWorkspaceId |
Shares toll-free number from verified workspace |
| Inherit + New Number | sourceWorkspaceId + inheritWithNewNumber => true |
Copies business info, purchases new number |
| Fresh | verification => [...] |
Full business details, new number + carrier approval |
Workspace Management
Credits & API Keys
Webhooks & Analytics
Full enterprise docs: sendly.live/docs/enterprise
License
MIT
All versions of sendly-php with dependencies
guzzlehttp/guzzle Version ^7.15.2
psr/http-client Version ^1.0
guzzlehttp/psr7 Version ^2.12.3