Download the PHP package whatsdev/whatsdev-php without Composer
On this page you can find all versions of the php package whatsdev/whatsdev-php. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download whatsdev/whatsdev-php
More information about whatsdev/whatsdev-php
Files in whatsdev/whatsdev-php
Package whatsdev-php
Short Description Official PHP client for the WhatsDev WhatsApp API: sessions, messaging, contacts, bulk sends, templates and signed webhooks
License MIT
Homepage https://whats.youdev.online/docs
Informations about the package whatsdev-php
WhatsDev PHP SDK
The official PHP client for the WhatsDev WhatsApp API.
Installation
Requires php >= 8.2 with the curl and json extensions — nothing else. That's the whole runtime dependency list. Anything beyond that (Illuminate, testbench, PSR interfaces) is dev-only and never lands in a consumer's vendor/ for production.
Quickstart
This is real, copy-pasteable code that runs against the sandbox with a test key as-is:
A sandbox session never needs a QR scan or a real WhatsApp number, but it takes a few seconds to reach connected after creation — that's what the third line is waiting for. Every message sent through it is simulated end-to-end and never actually reaches WhatsApp.
Response shapes: the data wrapper
Every endpoint that returns a single resource — a session, a message, the account — wraps its payload under an outer data key on the server, exactly as the API reference documents it. The package does not strip that wrapper for you: a call returns the response body exactly as it arrived, data key included. This is deliberate, not an oversight — not every endpoint wraps the same way, and a "helpful" auto-unwrap would have to guess the shape instead of reflecting it, so what you read in your code would stop matching what the API docs say. That's why the quickstart reads $session['data']['id'], not $session['id']. The one exception is list() endpoints: the Paginator hands you the items themselves as you iterate, never the list envelope's own data key — see Pagination below.
Authentication, and test vs. live keys
Every request carries Authorization: Bearer <key>. A test key and a live key are exactly the same shape — there is no test_/live_ prefix the client can look for. The mode is a flag WhatsDev stores against the key server-side, not something encoded in the key material, so the SDK never tries to infer it from the string. There is also no endpoint that reveals it — not account()->me(), nor anything else. The mode you know is the one you chose when you created the key: it's shown on your account dashboard at creation time, so keep track of it there.
Get a test key from your account dashboard (API Keys → create a new key with the "Test" toggle on) and use it while developing: it runs inside a sandbox completely isolated from your real account and never consumes real quota. Only switch to a live key when you go to production.
Don't serialize a configured client. var_dump(), print_r() and json_encode() on the config all print ***redacted*** in place of the key, but serialize(), var_export() and reflection read it in full. That is deliberate: a Client held as a property of a queued job is serialized into the queue payload and has to still work after unserialize, so a redacting __serialize() would hand that job a broken key and fail silently later. Keep a client out of queue payloads and cache entries — build it where it is used, from your own configuration — and the key never lands in your queue store.
Error handling
Every failure that comes from the API, the network or a webhook extends WhatsDev\Exceptions\WhatsDevException — API errors, connection errors, and the webhook and header failures alike. API errors (any non-2xx response) come back as a subclass of ApiException matching the server's error code — for example QuotaExceededException when a daily or monthly send quota is exhausted. That lets you catch a specific failure without string-matching a message:
An error code this version of the package doesn't recognise (because the server added it later) never breaks anything — it arrives as a plain ApiException instead of a typed one, so your code keeps working against a newer API.
Three failures sit outside that hierarchy on purpose: constructing a Client with an empty API key, passing request() an option key it doesn't recognise, and passing it a body that isn't an array or null. All three are wrong arguments rather than anything the API did, and all three are raised before a request exists, so all three throw PHP's own InvalidArgumentException. The dividing line is what went wrong, not when: the last two are raised with a perfectly good client already in hand.
Pagination
Every list() method returns a Paginator you can iterate directly with foreach — it fetches the next page automatically, so you never write paging logic by hand:
No request is issued until you actually start iterating (it's lazy), and the loop stops itself after the last page.
Downloading media
An inbound media message has its file stored on the server. media() is the one method in the package that doesn't return decoded JSON — it hands back the bytes exactly as they were streamed, plus the content type the server named them:
$file->contents is the raw bytes as a string and $file->contentType is the Content-Type header or null. Decoding a picture as JSON would have discarded the only thing you asked for, which is why this one method has a return type of its own.
Webhook verification
When a webhook event arrives, verify the X-Signature header before trusting anything in the body:
An empty secret throws — it does not return false. If $webhookSecret is an empty string — exactly the shape of an unset environment variable — both verify() and assert() throw MissingWebhookSecretException. An HMAC keyed on an empty string is a perfectly valid digest an attacker can compute, so returning false would send you hunting a forged payload that never existed instead of finding your own missing configuration.
This is the part to get right: pass the raw body exactly as received, never a value that has been through json_decode and back through json_encode. The signature is computed over the exact bytes the server sent — decoding and re-encoding can change key order or whitespace, and that alone makes verification fail for a perfectly genuine delivery. That's why verify()/assert() only accept a string, never an array or object. The comparison itself runs in constant time via hash_equals, so execution time never leaks whether the signature was close to matching.
The escape hatch (request())
Any endpoint the package doesn't have a dedicated method for is still one call away:
The third argument is an options array keyed query, body, headers and idempotencyKey. Any other key throws InvalidArgumentException rather than being ignored, so a mistyped bodyy can't quietly send no body at all. Most of what the escape hatch reaches are writes, and a write takes a body:
Retries follow the same rule here as everywhere else in the package, and that rule is not "always". A GET is retried on a connection error and on 429, 502, 503 and 504. A write — POST, PUT, PATCH, DELETE — is retried only when it carries an Idempotency-Key, because replaying a write the server may already have accepted could deliver a real message twice. The dedicated send methods generate that key for you; the escape hatch does not, so pass one yourself whenever you want a write to be retryable:
Everything else is shared with the rest of the package: authentication, the typed errors above, and the response body exactly as it arrived, data wrapper included. So nothing in the API is ever unreachable from the SDK.
Swapping in your own HTTP client
withHttpClient() accepts any implementation of WhatsDev\Http\HttpClient, and the package ships Psr18Client so you can hand it a PSR-18 client you already have — Guzzle, for instance — alongside the psr/http-client and psr/http-factory packages its constructor needs.
Turn redirect-following off on whatever you inject. The bundled cURL client refuses a 3xx and raises UnexpectedRedirectException, but Psr18Client delegates the entire request: a client that follows a 307 or 308 itself re-sends the POST body to the redirect target, below the point where an Idempotency-Key would have deduplicated it, and the SDK never sees the redirect at all. With Guzzle that means new GuzzleHttp\Client(['allow_redirects' => false]).
Optional Laravel bridge
The package itself needs nothing from Laravel, but it ships an optional bridge that auto-discovers inside a Laravel project. Add to .env:
Then use the facade:
Or resolve WhatsDev\Client straight from the container — both return the same singleton. To publish the editable config file:
Note: the Client is built lazily, on first resolution — an app that installs the package but never touches it is never affected. The first call that does resolve it without WHATSDEV_API_KEY set throws a clear InvalidArgumentException instead of failing silently later.
All versions of whatsdev-php with dependencies
ext-curl Version *
ext-json Version *