Download the PHP package denlopes/waha-laravel-sdk without Composer
On this page you can find all versions of the php package denlopes/waha-laravel-sdk. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package waha-laravel-sdk
WAHA Laravel SDK
A Laravel client for WAHA, the self-hosted WhatsApp HTTP API. It talks to one or more WAHA servers through typed, injectable services and a small fluent layer for chat, message, and human-like sending.
What's in the box:
- Services for every API area (
MessagingService,SessionService,ChatsService, and the rest) that return typed DTOs instead of raw arrays. - Fluent
Chat,Message, andConversationhandles that carry their session and ID, so you don't repeat them on every call. - A
Conversationlayer that paces sends like a human and enforces quotas per contact stage, to avoid WhatsApp spam flags. - Multi-host routing from config or the database, with per-session API keys.
- Webhook verification, replay protection, and dispatch.
Table of contents
- Requirements
- Installation
- Quick start
- Sessions
- Services
- DTOs
- Configuration
- Multi-host
- Logging
- Webhooks
- Errors
- Architecture
- Coverage
- Testing
Requirements
- PHP
^8.3 - Laravel
^10.0 || ^11.0 || ^12.0 || ^13.0 - phpredis, if you use the Redis limiter driver. The conversation limiters are
built on
PhpRedisConnection; predis is not supported for them.
Installation
Package discovery registers the service provider and the Waha facade. Publish
the config and migrations:
Then set your WAHA URL and API key in .env:
That's enough to start sending. If you run the conversation limiters on Redis,
install the phpredis extension and set WAHA_CONVERSATIONS_LIMITER_DRIVER=redis.
Predis won't work for that driver.
Local development
When working on the package alongside an application, register it as a Composer path repository:
Quick start
The Waha facade is the fastest way in. Send a text message:
Send an image:
Reply to an inbound message:
Send like a human, with pacing and quotas handled for you:
List sessions through a service:
The facade resolves to DenLopes\Waha\Client. Prefer constructor injection?
Inject Client instead:
The second argument picks the session. Omit it to use waha.default_session.
Chat
Lookups:
Send methods — sendMessage, sendImage, sendFile, sendVoice, sendVideo,
sendPoll, sendLocation, sendContactVcard, sendList,
sendLinkCustomPreview, and forward — each return a Message.
State-changing actions return $this for chaining: startTyping(),
stopTyping(), react(), star(), markRead(), pinMessage(),
unpinMessage(), archive(), unarchive(), markUnread(), clearMessages(),
and delete().
Message
A Message is returned by every send* method and by message() / find():
Message exposes get() (the raw MessageData), refresh(), markRead(),
react(), star(), pin(), unpin(), update(), forward(), delete(), and
toArray() / toJson().
Conversations (anti-ban)
Conversation wraps a Chat and sends messages the way WAHA recommends to avoid
being flagged as spam:
Or build one from an existing chat and drive the lower-level steps yourself:
The behavior is driven by the waha.conversations config block. Transport
mechanics (thinking, typing, pauses, delay skew) stay on the Pacing value
object. Quotas live on per-stage TierConfig objects, one for cold, warm,
and reply. Pacing::tier(ContactStage::Cold) returns the tier for a stage.
Each send() gates the message through a fixed pipeline before anything goes
out:
- The delivery circuit breaker, if enabled. When a session's recent failure
rate clears the threshold,
send()throwsCircuitBreakerOpenException. - Stage resolution. A
replyTomakes it a reply; a contact that has messaged you before is warm; anything else is cold.withStage()forces a stage. - Cold link policy. Cold sends disable link previews, and can reject URLs with
ColdMessageContainsUrlException. - Reachout guard. Cold sends check the session's capping and timelock and throw
ReachoutQuotaExhaustedExceptionorReachoutTimelockActiveException. - Per-chat and per-session quotas for the stage, then the cooldown wait.
Pacing state is stored in Laravel's cache by default, so the cooldown and window
cap are shared across conversations targeting the same chat, even in different
workers. Each send() also takes a Cache::lock() scoped to that chat, so
concurrent sends serialize instead of racing on the shared state. The lock
time-to-live is computed from the message and pacing policy, so it grows with
longer messages rather than using a fixed timeout. How long a worker waits for
the lock is separate, controlled by lock_wait_seconds. Set cache_store and
cache_prefix in waha.conversations to pick the cache store and key namespace.
Call reset() to clear the state.
Inbound webhooks feed the model too. A 1:1 inbound message marks the contact warm, and message acks record delivery successes and failures for the circuit breaker. Both are handled by the package's webhook listener and no-op when the matching feature is disabled.
When these sends run inside a queue job, keep the worker timeout above the
longest possible delay. The thinking and typing pauses are synchronous usleep()
calls, so a very long message can block a worker for a minute or more. A
queue:work --timeout=120 is a safer floor than the default 30 seconds.
Sessions
A session name is wrapped in a Session value object so it can't be confused
with a chat ID, message ID, or phone number.
Most service methods accept ?Session $session = null and fall back to the
configured default when omitted.
Services
Below the fluent layer, each WAHA API area has its own injectable service.
Services follow a consistent naming convention: list*, get*, create*,
update*, delete*, send*, set*.
The full service list is in Coverage.
DTOs
- Request DTOs live in
src/Data/Inputand extendData. Construct them with named arguments and serialize them withtoArray()/toJson(). - Response and event DTOs live in
src/Data/Outputand are built from API arrays withfromArray()(orfromJson()).
The Data serializer walks public constructor-promoted properties, skips null
values (WAHA treats an omitted key as "leave unchanged"), and recursively
serializes nested DTOs, backed enums, and arrays. It also provides safe extraction
helpers — string(), arrayValue(), intValue(), boolValue() — used by
fromArray() mappers to degrade gracefully on unexpected payloads.
Configuration
All settings come from config/waha.php and the environment, read through
Laravel's config() helper.
| Config key | Env var | Default | Description |
|---|---|---|---|
waha.base_url |
WAHA_BASE_URL |
http://localhost:3000 |
Base URL of the WAHA server. |
waha.api_key |
WAHA_API_KEY |
(none) | Secret sent via the X-Api-Key header. |
waha.default_session |
WAHA_DEFAULT_SESSION |
default |
Session used when none is given explicitly. |
waha.timeout |
WAHA_TIMEOUT |
30 |
HTTP request timeout, in seconds. |
waha.connect_timeout |
WAHA_CONNECT_TIMEOUT |
5 |
TCP connection timeout, in seconds. |
waha.retry_attempts |
WAHA_RETRY_ATTEMPTS |
3 |
Retries for transient failures and connection errors on idempotent methods. |
waha.retry_delay_ms |
WAHA_RETRY_DELAY_MS |
200 |
Initial retry backoff in ms (exponential, with jitter). |
Multi-host and webhook settings live in their own sections below.
Multi-host
Define waha.hosts to talk to more than one WAHA server. When empty, the
single-host keys above are used as the primary host.
Host selection is abstracted behind HostRegistry, ApiKeyProvider, and
SessionRouter contracts. Hosts are normalized into an immutable HostConfig
value object, and the mode string is represented by the ApiKeyMode enum
(ADMIN_FALLBACK / STRICT_SESSION_KEY).
DB-backed hosts
Set WAHA_REGISTRY_DRIVER=db to read hosts from the waha_hosts table instead of
config. Run php artisan migrate, then seed the table. Each host is keyed by a
unique key and can define per-session API keys.
Session → host pinning
Set WAHA_ROUTING_DRIVER=pin to resolve the host from the waha_session_pins
table (session name → host key), falling back to default_host when unknown.
This is how each tenant gets its own WhatsApp number — and, as it grows, its own WAHA host — without hardcoding that mapping in the SDK.
Logging
The package merges two dedicated channels into the host application's logging
config: waha (request/response lifecycle) and wahaError (failures). Override
them in your own config/logging.php if you want different drivers, paths, or
levels.
Webhooks
When enabled (the default), the service provider registers a stateless route for
inbound WAHA deliveries. It verifies the request, parses it into a typed
Webhook, then dispatches it.
Route
Default endpoint: POST /webhooks/waha. Configure it with
waha.webhooks.route.prefix (WAHA_WEBHOOKS_ROUTE_PREFIX) and
waha.webhooks.route.middleware.
Verification
The controller checks, in order:
- HMAC signature —
X-Webhook-Hmacover the raw body usingwaha.webhooks.secret(WAHA_WEBHOOK_SECRET). The algorithm comes fromX-Webhook-Hmac-Algorithmand defaults tosha512. - Timestamp freshness —
X-Webhook-Timestampagainstwaha.webhooks.max_clock_skew_ms. - Replay de-duplication —
X-Webhook-Request-Idvia the cache forwaha.webhooks.replay.ttl_seconds.
Set WAHA_WEBHOOKS_REQUIRE_HMAC=false to accept unauthenticated deliveries
(not recommended outside development).
Handling
Two extension points:
- Laravel event —
DenLopes\Waha\Webhooks\Events\WebhookReceivedis always fired and carries the parsedWebhookplus the raw body and request ID. - Configured handlers — map WAHA event names to handler classes:
Handlers implement DenLopes\Waha\Webhooks\Contracts\WebhookHandler.
The package also registers its own listener for inbound messages and message acks. It marks contacts warm and feeds the delivery circuit breaker, and no-ops when those features are disabled. See Conversations (anti-ban).
Processing mode
sync(default) — runs handlers inline during the HTTP request.queue— dispatchesProcessWebhookJoband returns immediately (WAHA_WEBHOOKS_PROCESSING_MODE=queue).
Parsing
Webhook::fromArray() maps payload to the most specific DTO for the event
(e.g. MessageData for message). Unrecognized events keep their raw array.
Storage
Set WAHA_WEBHOOKS_STORE_ENABLED=true to persist verified deliveries to the
waha_webhook_events table.
Errors
Every failure is thrown as a subclass of DenLopes\Waha\Exceptions\WahaException,
so you can catch the base type for "any WAHA problem" or a specific subtype for
targeted handling. API/HTTP failures share ApiException as their base.
| Exception | Trigger |
|---|---|
ApiException |
Base for API/HTTP errors |
AuthenticationException |
401/403 |
CredentialsException |
Missing/invalid API key (extends AuthenticationException) |
SessionNotFoundException |
404 on a session-scoped endpoint |
NoDataException |
404 on a non-session resource |
RateLimitException |
429 |
RequestException |
400/422 |
ServerException |
5xx |
ConnectionException |
Connection failure or timeout |
IntegrationException |
JSON decode failures and unclassified failures |
NotImplementedException |
501 endpoint not implemented by the engine |
UnknownHostException |
Requested host is not configured |
WebhookException |
Webhook verification or dispatch failure |
ConversationThrottledException |
Per-chat window cap reached |
SessionRateLimitedException |
Per-session stage quota reached |
ColdFanoutThrottledException |
Cold unique-target budget exhausted |
ReachoutQuotaExhaustedException |
WhatsApp reachout capping exhausted |
ReachoutTimelockActiveException |
WhatsApp reachout timelock active |
ColdMessageContainsUrlException |
Cold message rejected for containing a URL |
CircuitBreakerOpenException |
Delivery circuit breaker open |
Each exception carries a structured context() array (HTTP method, endpoint,
status, and a response body snippet) for logging and diagnostics.
Architecture
Request layer
DenLopes\Waha\Http\HttpClient (bound to DenLopes\Waha\Contracts\HttpClient)
is the only place that talks HTTP. It:
- builds the Laravel HTTP client with the configured base URL and
X-Api-Key; - retries transient HTTP failures (
429,5xx) and connection errors for idempotent methods only, with exponential backoff plus jitter — writes are never retried, to avoid duplicate messages; - sends JSON requests and decodes the response;
- downloads binary responses (QR images, screenshots, media) and negotiates the
binary representation via the
Acceptheader; - translates HTTP failures into typed exceptions.
SendsRequests is the trait consumed by every service. It injects the HTTP
client through the constructor and provides send() and download() helpers
that normalize failures into domain exceptions.
The HTTP client records its last request and response in DebugStore, which is
useful for troubleshooting:
Coverage
The service layer covers every area exposed by the WAHA OpenAPI document:
| Service | Area |
|---|---|
SessionService |
Session lifecycle and info |
PairingService |
QR, code, passkey pairing, screenshots |
ProfileService |
Profile name/status/picture |
MessagingService |
Sending messages and reactions |
ChatsService |
Chats, messages, pinning, archiving |
GroupsService |
Group management and settings |
ContactsService |
Contacts and number checks |
LidsService |
LID ↔ phone number mappings |
LabelsService |
Labels (WhatsApp Business) |
ChannelsService |
Channels/newsletters |
StatusService |
Status (stories) |
PresenceService |
Presence management |
CallsService |
Call rejection |
EventsService |
Event (RSVP) messages |
MediaService |
Media conversion |
ApiKeysService |
API key management |
AppsService |
Built-in apps and the MCP endpoint |
ObservabilityService |
Ping, health, server, debugging |
Testing
The package uses Orchestra Testbench, so the suite runs standalone — no host Laravel application required.
WahaTestCase extends Orchestra\Testbench\TestCase and registers
WahaServiceProvider via getPackageProviders(), so Laravel-booted tests run
against an in-memory application.
All versions of waha-laravel-sdk with dependencies
guzzlehttp/guzzle Version ^7.5 || ^8.0
laravel/framework Version ^10.0 || ^11.0 || ^12.0 || ^13.0