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.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package sendly-php

Sendly PHP SDK

Packagist license

Sendly PHP SDK

Official PHP SDK for the Sendly SMS API.

Requirements

Installation

Quick Start

Prerequisites for Live Messaging

Before sending live SMS messages, you need:

  1. Business Verification - Complete verification in the Sendly dashboard

    • International: Instant approval (just provide Sender ID)
    • US/Canada: Requires carrier approval
  2. Credits - Add credits to your account

    • Test keys (sk_test_*) work without credits (sandbox mode)
    • Live keys (sk_live_*) require credits for each message
  3. 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_shortener rollout flag and is not yet generally available; until the flag is enabled for your account the endpoints read as absent and calls throw NotFoundException (HTTP 404).

WhatsApp

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 404 not_found (thrown as NotFoundException), and a 'channel' => 'whatsapp' send throws SendlyException (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.

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 answer rcs_not_enabled). RCS sends and capability checks require a live API key. Registration reads need an API key with the rcs:read scope and writes the rcs:write scope.

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 from number 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 throw NotFoundException (voice_not_enabled). Reads need the calls:read scope, writes calls:write and 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() and recipients() have no route on the API and fail with a 404. Read the campaign with get() 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

PHP Build Version
Package Version
Requires php Version >=8.1
guzzlehttp/guzzle Version ^7.15.2
psr/http-client Version ^1.0
guzzlehttp/psr7 Version ^2.12.3
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package sendly/sendly-php contains the following files

Loading the files please wait ...