mediaburst/clockworksms
A PHP client for the Clockwork SMS API that lets you send single or bulk text messages, check credits, and handle delivery reports. Easy to integrate into Laravel apps for notifications, alerts, and two-factor codes.
This is a thin, framework-agnostic wrapper around the Clockwork (Mediaburst) SMS HTTP API. There is no Laravel package, service provider, facade, or config file — you get two classes:
Mediaburst\Clockwork\Clockwork — the HTTP client (auth + transport)Mediaburst\Clockwork\Message — the message value object you build and hand to the clientcomposer require mediaburst/clockworksms
Read the README first — it is short and is the only real documentation you get. Then open src/Clockwork.php and src/Message.php in vendor/mediaburst/clockworksms/; there are only a handful of public methods and skimming them answers most "does it support X?" questions. If the namespace in the README differs from the one you expect, check composer.json's autoload map — the package was renamed at one point.
Sign up at clockwork.io (or use the Mediaburst dashboard) and go to Settings → API keys. The key is what you pass to the constructor.
use Mediaburst\Clockwork\Clockwork;
use Mediaburst\Clockwork\Message;
$clockwork = new Clockwork(config('services.clockwork.key'));
$message = new Message('+441234567890', 'Your appointment is confirmed.');
$message->setFrom('MYBRAND'); // 11 alphanumeric chars or a long number
$message->setReference('appt-88213'); // echoed back in delivery reports
$result = $clockwork->send($message);
if ($result['success']) {
$messageId = $result['id'];
} else {
// $result['error'] / $result['errno']
}
That is the entire happy path. Everything else is either message options (setFrom, setReference, setTtr, setInvalidCharacterAction, setConcat, setFlash) or the reporting endpoints (getBalance, getDeliveryStatus, getMessages).
The realistic first thing you will build is "fire-and-forget SMS from a queued job" — order confirmation, OTP, appointment reminder. Keep the send inside a queued job and treat the returned id as a correlation handle for the delivery report.
Do not scatter new Clockwork(...) through your app. The package has no DI hook, but a one-line binding is enough:
// App\Services\ClockworkSms
class ClockworkSms
{
public function __construct(private Clockwork $client) {}
public function send(string $to, string $text, array $opts = []): array
{
$message = new Message($to, $text);
if (isset($opts['from'])) $message->setFrom($opts['from']);
if (isset($opts['reference'])) $message->setReference($opts['reference']);
if (isset($opts['ttr'])) $message->setTtr($opts['ttr']);
return $this->client->send($message);
}
}
// AppServiceProvider
$this->app->singleton(Clockwork::class, fn () =>
new Clockwork(config('services.clockwork.key'))
);
$this->app->singleton(ClockworkSms::class);
The client is stateless apart from the key, so a singleton is safe and keeps a single HTTP connection pool.
SMS providers have variable latency and the wrapper does a blocking Guzzle call. A job with $tries = 3 and $backoff = 30 gives you natural retry semantics:
public function handle(ClockworkSms $sms)
{
$result = $sms->send($this->to, $this->text, ['reference' => $this->order->id]);
if (! $result['success']) {
// 401/402-class errno → don't retry, it's your fault (bad key, no credit)
if (in_array($result['errno'] ?? 0, [9, 10, 25])) {
$this->fail(new \RuntimeException($result['error']));
}
$this->release(30);
}
}
Never retry on success = false without inspecting errno — bad key or zero balance will burn your whole queue retry budget.
send() accepts an array of Message objects too, and fans them out in one HTTP round trip:
$messages = collect($subscribers)->map(
fn ($s) => (new Message($s->phone, $body))->setReference($campaign->id)
)->all();
$results = $clockwork->send($messages);
Each element of $results has its own success/id, so match by index, not by phone number (duplicates are legal). Bulk reduces latency but not cost — you are still charged per segment.
Keep the id returned from send() and use setReference() to carry your own identifier alongside it. When a delivery report webhook or a manual getDeliveryStatus($id) call arrives, you can find the row by either. Store both columns:
SmsLog::create([
'provider_id' => $result['id'],
'reference' => $order->id,
'status' => 'sent',
]);
getMessages($start, $end) accepts dates and returns recent message history — use it for reconciliation jobs rather than as your primary record.
Call getBalance() on a scheduled command daily and alert when it drops below your threshold. Credits are consumed per 160-character GSM segment, so a 320-char message costs 2 credits. Log strlen($text) alongside the send so you can reconstruct cost later.
The last tag is from November 2017. That predates Guzzle 7 and PSR-18. Check composer.lock immediately after install: on a modern Laravel you will often hit a constraint conflict on guzzlehttp/guzzle. The pragmatic fixes, in order of preference:
composer.json edit).replace / provide shim in your root composer.json.--ignore-platform-reqs as a first resort; the failure is usually an interface change, not just a version number.Do not assume PHP 8 compatibility. Run the send path once in CI against a real key (or a fake HTTP client) before trusting it in production.
The client is constructed with a key and calls out immediately. For tests, bind Clockwork to a MockHandler-backed Guzzle client if the class exposes injection, otherwise wrap sends in your ClockworkSms class and swap that binding in tests. Do not let real HTTP leave your test suite.
errno is more useful than the stringThe API returns errno (an integer) alongside error. Branch on errno, log the string. The important classes:
Mapping table lives in the Clockwork API docs, not in the package.
The default character set handling is GSM-7. If you send anything with emoji, curly quotes, or accented characters you will silently pay for UCS-2 segments (70 chars per credit) unless you set how invalid characters are handled. For OTPs and alerts, force ASCII at the source — do not rely on the library to transliterate.
$message->setInvalidCharacterAction(Message::INVALID_CHARACTER_REPLACE);
Use INVALID_CHARACTER_ERROR in staging to catch stray Unicode before it hits the bill.
Alphanumeric from values (e.g. MYBRAND) work in the UK and most of Europe but are dropped or rejected for US/Canada and some Asian routes. If you are international, either use a provisioned long code or accept that from may be silently ignored and the message arrives from a shared short code.
to number needs a country code+441234567890 — no leading zero after the country code, no spaces, no brackets. The wrapper will happily send garbage and return success: false with a low-level error; you will waste time debugging "why is my error empty" only to find the number was 01234 567 890.
The wrapper constructs its own HTTP client internally (or accepts one, depending on version). You almost certainly cannot set a per-request timeout from the public API. If a provider outage is a business risk, wrap the Clockwork singleton in a decorator that enforces a timeout via a custom Guzzle handler, or catch ClockworkException / GuzzleException at the job boundary and release with backoff.
There is no middleware, no event, no response object — you get associative arrays. If you need richer behaviour (structured exceptions, metrics, tracing), build it in your ClockworkSms wrapper, not in the package. Treat the vendor folder as read-only and keep the adapter thin so you can swap to the raw HTTP endpoint, or to a maintained alternative like vonage/client or twilio/sdk, without touching call sites.
45 stars, zero dependents, no release since 2017. If this is a greenfield project, look at Clockwork's raw REST API — it is a simple JSON POST and you can implement the four calls you actually need in ~80 lines with Http::withHeaders(...)->post(...). That gives you modern Guzzle, timeouts, retries, Http::fake(), and no dependency archaeology. Reserve this package for codebases that already use it and just need maintenance.
How can I help you explore Laravel packages today?