Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sendly-php-client

PHP client for the Sendly SMS API (https://api.sendly.link).

  • Zero Composer runtime dependencies (transport is based solely on ext-curl), strict types, readonly result classes.
  • PHP 8.2+.
  • No automatic retries: a retried request could send an SMS twice, so the client never retries requests on its own. If you retry sending on the application side, make sure first that the previous attempt definitely failed before the request was accepted.

Requirements

  • PHP >= 8.2 with the curl and json extensions.

Installation

composer require sendly/php-client

The package is not yet published on Packagist – until then you can install it directly from the repository by adding this to your project's composer.json:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/sendly-link/php-client"
        }
    ],
    "require": {
        "sendly/php-client": "^2.0"
    }
}

Configuration

Setting Constructor argument Environment variable Default value
API token $token SENDLY_TOKEN – (required)
Base URL $baseUrl SENDLY_BASE_URL https://api.sendly.link
HTTP timeout (s) $timeout 30.0

Arguments passed explicitly to the constructor take precedence over environment variables. Every SENDLY_* variable has a legacy fallback: when SENDLY_TOKEN / SENDLY_BASE_URL is not set, ACTIO_TOKEN / ACTIO_BASE_URL is read instead; when both are set, SENDLY_* wins. A missing token (empty argument and no environment variables) raises SendlyValidationException. The token is generated in the Sendly client panel (or provided by customer support).

Quick start – sending a single SMS

use Sendly\Client\SendlyClient;

$client = new SendlyClient('your-api-token'); // or set SENDLY_TOKEN

$result = $client->sendSms(from: '48732129000', to: '48732129001', body: 'Test Sendly');
echo $result->messageId; // unique message id, e.g. "a906cff7719bd889"

Equivalent in curl:

curl -X POST https://api.sendly.link/api/sms \
  -H "Authorization: Bearer $SENDLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"from": "48732129000", "to": "48732129001", "body": "Test Sendly"}'

The message_id field (snake_case in the API) is exposed throughout the client as messageId (single send, sendSmsMulti result entries, and NOTIFICATION webhook events).

Sending to multiple numbers

$results = $client->sendSmsMulti(
    from: '48732129000',
    to: ['48732129001', '48732129002'],
    body: 'Test Sendly',
);
foreach ($results as $item) {
    echo $item->number . ' ' . $item->messageId . PHP_EOL;
}

Equivalent in curl:

curl -X POST https://api.sendly.link/api/sms-multi \
  -H "Authorization: Bearer $SENDLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"from": "48732129000", "to": ["48732129001", "48732129002"], "body": "Test Sendly"}'

to must contain 1–100 unique numbers. Server-side validation works on an "all or nothing" basis: every number must be a valid Polish mobile number, and the numbers must be unique – otherwise the entire request is rejected. This API method must additionally be enabled for your token by Sendly customer support.

Parsing webhooks

The Sendly SMS API sends incoming messages via POST, as well as (once enabled) delivery notifications, to the webhook address configured in the client panel. Delivery is a single attempt, redirects are not followed, and no authentication is performed against your webhook; requests originate from the current IP address of the api.sendly.link host.

Webhook::parse accepts the raw request body as a string or decoded JSON as an array, and returns a typed model:

use Sendly\Client\DeliveryNotification;
use Sendly\Client\IncomingMessage;
use Sendly\Client\Webhook;

$event = Webhook::parse(file_get_contents('php://input'));

if ($event instanceof IncomingMessage) {
    // {"type": "MESSAGE", "from": ..., "to": ..., "body": ...}
    printf("SMS from %s to %s: %s\n", $event->from, $event->to, $event->body);
} elseif ($event instanceof DeliveryNotification) {
    // {"type": "NOTIFICATION", "message_id": ..., "status": "DELIVERED" | "ERROR"}
    printf("Message %s is %s\n", $event->messageId, $event->status->value);
}

Example payloads:

{"type": "MESSAGE", "from": "48732129000", "to": "48732129001", "body": "Test sms"}
{"type": "NOTIFICATION", "message_id": "a906cff7719bd889", "status": "DELIVERED"}

Invalid payloads and unknown type/status values raise SendlyValidationException. Note that IncomingMessage::$from can be an alphanumeric sender name (a sender field override), not just a string of digits. The delivery status is the DeliveryStatus enum (DELIVERED / ERROR).

Error handling

All library errors extend SendlyException (which extends RuntimeException):

Exception When
SendlyValidationException Invalid input detected client-side, before any HTTP request is sent; also invalid webhook payloads.
SendlyApiException The API responded with an error status (403 authorization problem, 422 validation problem).
SendlyException Network errors (DNS, connection, timeout – with curl error info) and unexpected 200 responses with an invalid structure.

Client-side validation: from/to must be digit strings 9–11 characters long, body must not be empty, to in a multi-send must contain 1–100 unique numbers.

SendlyApiException carries statusCode, a parsed errors map (optional keys token, from, to, body, each a list of messages), and rawBody with the raw response body text – a diagnostic fallback that is always available, regardless of the shape of the decoded JSON (or when the response is not JSON at all).

use Sendly\Client\SendlyApiException;
use Sendly\Client\SendlyClient;
use Sendly\Client\SendlyValidationException;

$client = new SendlyClient('your-api-token');

try {
    $client->sendSms(from: '48732129000', to: '48732129001', body: 'Test Sendly');
} catch (SendlyValidationException $exception) {
    echo 'Bad input, nothing was sent: ' . $exception->getMessage();
} catch (SendlyApiException $exception) {
    if ($exception->statusCode === 403) {
        echo 'Authorization problem: ' . implode('; ', $exception->errors['token'] ?? [(string) $exception->rawBody]);
    } elseif ($exception->statusCode === 422) {
        foreach ($exception->errors as $field => $messages) {
            echo $field . ': ' . implode('; ', $messages) . PHP_EOL;
        }
    } else {
        echo $exception->getMessage();
    }
}

Request identification headers

Every outgoing request to the API carries, alongside Authorization and Content-Type:

  • User-Agent: sendly-php-client/<version> (PHP/<major>.<minor>), e.g. sendly-php-client/2.0.0 (PHP/8.2) – the version comes from the SendlyClient::VERSION constant;
  • X-Request-Id: <uuid> – a fresh UUID v4 for every request (generator based on random_bytes, no external dependencies), a correlation identifier for contacting customer support and investigating suspected double sends (complementing the no-retry policy).

Encoding

Outgoing messages are encoded as UCS2 and split every 60 characters. GSM7 encoding can optionally be enabled (via the client panel / customer support) with a single-message limit of up to 160 characters.

No automatic retries

The client never retries failed requests – a retry could send an SMS twice. If you need retries, implement them yourself with idempotency in mind. HTTP redirects are not followed either.

Out of scope

3CX SMS API mode (/api/tcx) is mutually exclusive with the REST SMS API and is not implemented in this client.

Development

# Composer (if not installed globally):
curl -sS https://getcomposer.org/installer | php -- --install-dir=. --filename=composer.phar

php composer.phar install
php composer.phar test   # or: vendor/bin/phpunit

Tests run against a local HTTP stub server (php -S 127.0.0.1:<port>) and never call the real API; the real curl transport is fully exercised in the process.

License

MIT – Copyright (c) 2026 ACTIO. See LICENSE.

About

PHP client for the Sendly SMS API – send SMS, bulk send, webhook parsing. Zero dependencies, PHP 8.2+.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages