Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

User Api V1 Laravel Package

zadarma/user-api-v1

A PHP client for the Zadarma User API v1, making it easy to integrate Zadarma's VoIP, PBX, SMS, and call-management features into your applications. It simplifies authentication and HTTP requests, returning API responses in a convenient format for PHP and Laravel projects.

View on GitHub
Deep Wiki
Context7

Getting Started

zadarma/user-api-v1 is a single-class wrapper around the Zadarma REST API (VoIP, PBX, SMS, call tracking, balance, documents). It is not Laravel-aware — no service provider, no facade, no config, no namespaced PSR-4 code. Treat it as a low-level HTTP client you drop behind your own abstraction.

Install and inspect first

composer require zadarma/user-api-v1

Then open vendor/zadarma/user-api-v1/ZadarmaApi.php before writing any code. It is ~200 lines: a constructor, a call() method, and two private transports (runCurl, runFileGetContents). Reading it once tells you everything you need to know about signing, error shapes, and limits — there is no documentation beyond the README and the Zadarma API docs.

The constructor is effectively:

$api = new \ZadarmaApi($key, $secret, 'https://api.zadarma.com', 'en', true);
//                                        api url (optional)  lang   useCurl

useCurl = true is strongly recommended; the fallback path uses file_get_contents with stream context and is harder to configure (no timeouts you control, no easy debugging).

First use case: verify credentials and balance

Zadarma’s docs recommend starting with /v1/info/balance/. It is read-only, cheap, and proves your key/secret/signature are correct:

$result = $api->call('/v1/info/balance/', [], 'GET');
if ($result['status'] === 'ok') {
    logger()->info('Zadarma balance', ['amount' => $result['balance']]);
}

Where to look when things break

  • The sign header/query parameter. If you get 401 Unauthorized or signature mismatch, the problem is almost always parameter ordering, URL-encoding, or a body/query mismatch — not your credentials.
  • format=json. The package injects this; if you forget to call through the class and build your own request, you get XML by default.
  • The status field in the decoded response body (ok vs error) — HTTP 200 does not mean success.

Implementation Patterns

Register as a singleton behind an interface

Do not inject ZadarmaApi directly. Wrap it, so you can swap it out (the package has 0 dependents and no release since Jan 2023 — assume you may need to replace it).

// app/Services/Zadarma/ZadarmaClient.php
namespace App\Services\Zadarma;

use ZadarmaApi;

class ZadarmaClient
{
    public function __construct(private array $config) {}

    public function raw(): ZadarmaApi
    {
        return new ZadarmaApi(
            $this->config['key'],
            $this->config['secret'],
            $this->config['url'] ?? 'https://api.zadarma.com',
            $this->config['lang'] ?? 'en',
            true // always curl
        );
    }

    public function call(string $path, array $params = [], string $method = 'GET'): array
    {
        $response = $this->raw()->call($path, $params, $method);
        // Normalise: the package returns a decoded array already
        if (($response['status'] ?? null) === 'error') {
            throw new ZadarmaException($response['message'] ?? 'Zadarma error', $response);
        }
        return $response;
    }
}

Bind in AppServiceProvider::register():

$this->app->singleton(ZadarmaClient::class, fn ($app) => new ZadarmaClient(
    config('services.zadarma') // key, secret, lang, url
));

Config in config/services.php:

'zadarma' => [
    'key'    => env('ZADARMA_KEY'),
    'secret' => env('ZADARMA_SECRET'),
    'lang'   => env('ZADARMA_LANG', 'en'),
],

Thin, focused service methods

Don’t expose call('/v1/sms/send/', ...) from controllers. Wrap each business operation:

class SmsService
{
    public function __construct(private ZadarmaClient $client) {}

    public function send(string $from, string $to, string $message): array
    {
        return $this->client->call('/v1/sms/send/', [
            'sender' => $from,
            'number' => $to,
            'message' => $message,
        ], 'POST');
    }
}

GET vs POST matters: Zadarma signs the concatenation of method + params + md5(params), so switching verbs changes the signature — always match the documented verb for each endpoint.

Queue outbound side effects

SMS sending, PBX callbacks (/v1/pbx/...), and call tracking triggers are network-bound and rate-limited. Push them through a job:

class SendSmsJob implements ShouldQueue
{
    use Queueable;
    public $tries = 3;
    public $backoff = [10, 60, 300];

    public function handle(SmsService $sms): void
    {
        $sms->send($this->from, $this->to, $this->message);
    }
}

Wrap calls in ZadarmaException so failed() can log and alert without leaking secrets.

Inbound webhooks — verify before trusting

Zadarma’s call tracking and SMS delivery callbacks arrive as application/x-www-form-urlencoded POST with a sign field. The package does not provide inbound verification. Implement it yourself:

$params = $request->post();
$sign = $params['sign'] ?? '';
unset($params['sign']);

// Per Zadarma docs: sort params, concatenate values, HMAC-SHA1 with SECRET
$check = base64_encode(hash_hmac('sha1', implode('', $params), config('services.zadarma.secret'), true));

abort_unless(hash_equals($check, $sign), 403);

This is exactly the class of thing the package does not abstract away — read the Zadarma webhook docs, not the package source, for this logic.

Logging and observability

You lose all visibility inside ZadarmaApi (it silently builds the URL and calls curl). Wrap call() with Log::debug() of the endpoint (never the secret) and elapsed time, and emit a metric on failure. This is the only practical observability hook the package gives you.

Gotchas and Tips

  • No namespace, global class. Autoloading relies on Composer’s classmap. In a Laravel app this means you cannot use ZadarmaApi without use ZadarmaApi; at the top of each file — or wrap it once (recommended) so the name appears only in one file.
  • call() returns an array, not an object. It decodes JSON internally. status => error responses are not exceptions — always check status and treat absence of the key as a problem too (some 4xx bodies are empty).
  • Signature is fragile. Params passed to the class are sorted and URL-encoded in a specific way (+ and %2F are rewritten). Do not pre-encode values, do not nest arrays, and do not rely on PHP’s array ordering. If signing fails, dump the exact query string the class builds (temporarily) and compare with Zadarma’s signer sample in their docs.
  • format=json is injected as a param. Do not pass format yourself — you will confuse the signature.
  • No timeouts on the fallback transport. If curl is missing, file_get_contents will hang on a slow endpoint. Force true for $curl and pin CURLOPT_TIMEOUT by subclassing or rewriting rather than relying on the default.
  • Language affects error messages. Passing 'ru' changes the message field text. Parse on Zadarma error codes, not message strings.
  • No retry, no rate-limit awareness. Zadarma rate-limits by endpoint; the package will just get a 429 and return it as an error status. Add backoff in your job wrapper.
  • Unmaintained since Jan 2023, 0 dependents. Budget for replacement. The signature algorithm and endpoint list are small enough that a ~40-line Guzzle client is a viable fallback if the package breaks on PHP upgrade — keep your wrapper interface narrow (one call() method) so the swap is a one-file change.
  • Secrets in exceptions. The underlying class throws \Exception with a message that may include the URL containing the sign parameter. Never surface ZadarmaException raw to clients — wrap with a domain exception and log the detail server-side only.
  • Extension points are effectively zero. No events, no middleware, no interceptors. Your integration seams are the wrapper constructor, the wrapper call(), and the job layer. Design those three layers well and the package becomes an implementation detail.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
cgv/localized-resource