Search by

mambusrl / spoki-whatsapp

supportoMambu

PHP client for the Spoki REST API

Package info

github.com/MambuSRL/spoki_whatsapp

pkg:composer/mambusrl/spoki-whatsapp

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-09-10 14:13 UTC

This package is auto-updated.

Last update: 2026-09-10 14:20:19 UTC


README

PHP 8.3+ client for the HTTP operations in the Spoki documentation, reviewed on September 10, 2026: 123 request examples and 115 distinct endpoints. Includes message variants, deprecated partner operations still listed in the reference, and V2 webhook signature verification. A documented endpoint may have been retired; check the method's deprecation notes before using it.

Installation

Requires PHP 8.3+ with curl, json and ctype. No external PHP dependencies. Both Composer PSR-4 and standalone autoloading are supported:

require '/path/to/spoki_whatsapp/autoload.php';

use MambuSRL\Spoki\Spoki;
use MambuSRL\Spoki\SpokiException;

$spoki = new Spoki((string) getenv('SPOKI_API_KEY'));

For Composer, configure the appropriate VCS/path repository and require mambusrl/spoki-whatsapp, then load vendor/autoload.php. The package does not need to be published on Packagist.

Resource Classes

Each callable documentation folder has its own class under MambuSRL\Spoki\Api. Partner subfolders use the nested MambuSRL\Spoki\Api\Partners namespace. Client owns authentication and HTTP transport; ApiResource shares client access and URL construction without exposing unrelated API operations on each resource.

Use the classes directly when your application needs only a few resources:

use MambuSRL\Spoki\Client;
use MambuSRL\Spoki\Api\Contacts;
use MambuSRL\Spoki\Api\Messages;

$client = new Client((string) getenv('SPOKI_API_KEY'));
$contacts = new Contacts($client);
$messages = new Messages($client);

$contacts->syncContact(['phone' => '+393331234567', 'first_name' => 'Alex']);
$messages->sendMessageText('+393331234567', 'Your order is ready.');

Alternatively, Spoki provides typed accessors and caches resource instances:

$spoki = new Spoki((string) getenv('SPOKI_API_KEY'));
$contacts = $spoki->contacts()->listContacts(['page' => 1]);
$spoki->messages()->sendMessageText('+393331234567', 'Hello');
$spoki->automations()->startAutomation($uuid, $secret, '+393331234567');

All resources created from the same client share its API key, timeout and transport. Changing the key with setApiKey() also affects existing resource instances.

Documentation folder Class Accessor
Accounts Api\Accounts accounts()
Agencies Api\Agencies agencies()
Automations Api\Automations automations()
Campaigns Api\Campaigns campaigns()
Contacts Api\Contacts contacts()
Custom Fields Api\CustomFields customFields()
Embedding via iframe Api\Embedding embedding()
Invitations Api\Invitations invitations()
Lists Api\Lists lists()
Messages Api\Messages messages()
Reports Api\Reports reports()
Roles Api\Roles roles()
Tags Api\Tags tags()
Templates Api\Templates templates()
Tickets Api\Tickets tickets()
Media Api\Media media()
Webhooks / API and signature verification Api\Webhooks webhooks()
Partners / Partners Api\Partners partners()
Partners / Partners / Deprecated Api\Partners\Deprecated deprecatedPartners()
Partners / Partner Roles Api\Partners\Roles partnerRoles()
Partners / Partner Invitations Api\Partners\Invitations partnerInvitations()
Channels Api\Channels channels()

Folders containing only incoming webhook payload examples, including "Webhook Contact in automation step", do not define callable endpoints and therefore do not get empty API classes. Receive and process those events in your application.

Compatibility

sendTemplate() and sendMessageText() retain their signatures, parameter order and JSON string return values. New API methods also return strings; a response without content, such as HTTP 204, returns ''.

The constructor accepts ($apikey = '', $apiBaseUrl = null, $timeout = 30, $transport = null). The default changes from https://app.spoki.it to the official https://api.spoki.com. An alternative HTTPS origin can be configured, without a path, query or credentials. Existing integrations can continue to include src/Spoki.php directly.

Existing flat calls such as $spoki->listContacts() are forwarded to their owning resource through __call(), preserving positional and named arguments. They are listed as @method annotations for IDE completion, not physically declared API methods on the facade. Code using ReflectionMethod or method_exists() to inspect individual API operations should inspect the resource class instead. New code should use the resource accessors or instantiate resource classes directly.

Requests default to a 30-second total timeout and a connection timeout of up to 10 seconds. Redirects are disabled and TLS verification remains enabled. All HTTP 2xx responses are accepted; invalid JSON raises an exception. There are no automatic retries, particularly to avoid duplicate message delivery.

Sending Messages

$messages = $spoki->messages();
$response = $messages->sendMessageText('+393331234567', 'Your order is ready.');
$result = json_decode($response, true, 512, JSON_THROW_ON_ERROR);

$messages->sendTemplate(
	templateId: 123,
	phone: '+393331234567',
	language: 'IT',
	customFields: ['ORDER_ID' => '42'],
	metadata: ['internal_id' => 100]
);

$messages->sendTemplateCarouselMultilanguage([
	'phone' => '+393331234567',
	'template' => 123,
	'header_media_set' => [
		['url' => 'https://example.com/image.jpg', 'filename' => 'image.jpg', 'language' => 'it'],
	],
	'custom_fields' => ['ORDER_ID' => '42'],
]);

sendMessage(array $data) accepts the complete documented payload, including optional fields such as email and channel-specific settings. Media, carousel, button and list helpers accept the complete payload and set the appropriate message type. Spoki requirements for templates, item counts, permissions and conversation windows still apply.

Automations

Starting an automation uses secret in the body, without an API key header. The UUID and secret belong to the automation's initial "API Url" step; the UUID is not the numeric automation ID.

$automations = (new Spoki())->automations();
$response = $automations->startAutomation(
	uuid: (string) getenv('SPOKI_AUTOMATION_UUID'),
	secret: (string) getenv('SPOKI_WEBHOOK_SECRET'),
	phone: '+393331234567',
	data: [
		'first_name' => 'Mario',
		'language' => 'it',
		'custom_fields' => ['ORDER_ID' => '42'],
		'metadata' => ['internal_id' => 100],
	]
);

$automations->startAutomationBulk(
	(string) getenv('SPOKI_AUTOMATION_UUID'),
	(string) getenv('SPOKI_WEBHOOK_SECRET'),
	[['phone' => '+393331234567'], ['phone' => '+393331234568']]
);

Listing, retrieving and creating automations requires the account API key, just like the other REST endpoints.

Payloads And Filters

list* methods accept a $query array containing documented filters and pagination. The client returns one page per call; it does not automatically fetch subsequent pages. Create and update methods accept the complete $data body without removing unknown fields. IDs can be integers or strings and are URL-encoded.

$contactsApi = $spoki->contacts();
$contacts = json_decode($contactsApi->listContacts(['page' => 1, 'search' => 'Mario']), true);
$contactsApi->syncContact(['phone' => '+393331234567', 'first_name' => 'Mario']);
$contactsApi->syncContacts([
	['phone' => '+393331234567', 'first_name' => 'Mario'],
	['phone' => '+393331234568', 'first_name' => 'Anna'],
]);
$contactsApi->updateContact(123, ['first_name' => 'Marco']);
$contactsApi->unblockContact(123);

$result = $spoki->requestJson('GET', '/api/1/contacts/', query: ['page' => 1]);

syncContacts() uses a JSON array at the request root. Other empty bodies are JSON objects. request() returns the original string; requestJson() decodes it and returns null for an empty response. Paths are relative to the origin, for example /api/1/contacts/, rather than absolute URLs.

Every method has English PHPDoc with a description, parameter types, payload or query fields where documented, return values, exceptions and a usage example. Field lists distinguish documented examples from explicitly required fields. Nested schemas vary by message type, automation step, trigger and platform; follow the linked API reference for those conditional requirements.

Available Methods

Complete signatures and per-method PHPDoc are preserved in the resource classes linked above. src/Spoki.php contains accessors and legacy delegation, not the endpoint implementations. src/Client.php owns the shared transport. The HTTP reference inventory is in tests/fixtures/endpoints.json.

Area Methods
Account listAccounts, getAccount, getAccountByPhone, getAccountCurrentReport, createAccountOnboardingLink
Agencies listAgencies, getAgency
Automations startAutomation, startAutomationBulk, listAutomations, getAutomation, createAutomation, getAutomationCustomFieldsUsed
Campaigns listCampaigns, createCampaign, getCampaign, updateCampaign, deleteCampaign
Contacts listContacts, syncContacts, syncContact, getContact, updateContact, deleteContact, addContactOperator, removeContactOperator, blockContact, unblockContact
Custom fields listCustomFields, createCustomField, getCustomField, updateCustomField, deleteCustomField
Iframe getAuthenticationToken
Invitations listInvitations, getInvitation, createInvitation, resendInvitation, updateInvitationRole, deleteInvitation
Lists listLists, createList, getList, syncListContacts, removeAllListContacts, removeListContacts, deleteList
Messages sendMessage, sendTemplate, sendMessageText, sendTemplateWithDynamicHeaderMedia, sendTemplateCarousel, sendTemplateCarouselMultilanguage, sendMessageWithButtons, sendMessageList
Report listReports, listDetailedReports
Roles listRoles, getRole, addServiceUser, generateRolePrivateKey, hasRolePrivateKey, updateRole, deleteRole
Tag listTags, getTag
Template listTemplates, createTemplate, cloneTemplate, updateTemplate, submitTemplate, templateBackToDraft, getTemplate, deleteTemplate
Ticket listTickets, createTicket, getTicket, updateTicket, deleteTicket
Media listMedia, createMedia, getMedia, updateMedia, deleteMedia
Webhook listWebhooks, createWebhook, getWebhook, updateWebhook, deleteWebhook, getWebhookExpands, setWebhookExpands, sendTestWebhook, rotateWebhookSecret, verifyWebhookSignature
Partner listPartners, listPartnerAccounts, createPartnerOnboardingLink, createAccountApiKey, revokeAccountApiKey, addSoftwareVendorClients, createMetaCreditSubrecharge, setAccountProfitMargins, moveCreditFromAccount, getPartnerAccountReport, getPartnerAccountForecasts
Deprecated partner operation createConversationsSubrecharge
Partner roles listPartnerRoles, getPartnerRole, generatePartnerRolePrivateKey, hasPartnerRolePrivateKey, updatePartnerRole, deletePartnerRole
Partner invitations listPartnerInvitations, getPartnerInvitation, createPartnerInvitation, resendPartnerInvitation, updatePartnerInvitationRole, deletePartnerInvitation
Channels listChannels, getChannel, createChannel, renameChannel, setPrimaryChannel, refreshWhatsAppPhoneStatus, getChannelByPhone

Undocumented CRUD operations, such as tag creation, are not invented. cloneTemplate uses GET as documented, even though it creates a copy. unblockContact sends PATCH with is_blocked: false: the Postman "Unlock Contact" example also includes ?is_blocked=true, which contradicts the body and is intentionally omitted. All /api/1/ endpoints use the general X-Spoki-Api-Key authentication scheme, even where an example omits the header.

The documentation marks createAccountOnboardingLink() as removed and returning 404; use createChannel(['platform' => 1]) instead. It also announces removal of listReports() on June 10, 2026; prefer listDetailedReports().

Errors

try {
	$response = $spoki->contacts()->listContacts();
} catch (SpokiException $exception) {
	$httpStatus = $exception->statusCode;
	$responseBody = $exception->responseBody;
	$retryAfter = $exception->responseHeaders['retry-after'] ?? null;
}

SpokiException extends RuntimeException and exposes the HTTP status (0 for transport errors), response body and response headers. Exception messages generated by the client do not include credentials or payloads. Response bodies may contain personal data: do not log them verbatim without filtering. Invalid input and JSON serialization failures raise InvalidArgumentException and JsonException, respectively. A missing required API key raises LogicException.

Rate limits vary by endpoint; handle HTTP 429 and Retry-After in the caller. A timeout does not prove that a message was not sent: do not retry automatically without checking the outcome.

Incoming Webhooks

Postman examples with method VIEW describe incoming notifications, not callable APIs. The application server must receive and process them. For V2 webhooks:

$rawBody = file_get_contents('php://input');
$valid = \MambuSRL\Spoki\Api\Webhooks::verifyWebhookSignature(
	$rawBody,
	$_SERVER['HTTP_X_SPOKI_SIGNATURE'] ?? '',
	(string) getenv('SPOKI_WEBHOOK_SIGNING_SECRET')
);
if (!$valid) {
	http_response_code(401);
	return;
}
$event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);

The helper verifies HMAC-SHA256 over the original body, uses a constant-time comparison and allows a 300-second timestamp tolerance. Store processed event_uuid values to prevent duplicates even within that window. Deprecated V1 signatures are not accepted. The webhook signing secret is different from the automation trigger secret.

Tests

docker exec -i php_local php /var/www/html/spoki_whatsapp/tests/run.php

Tests run offline and check endpoint coverage, payloads, authentication, queries, compatibility, HTTP and cURL failures, timeouts and webhook signatures without sending messages. Every resource method is compared with its legacy facade call, including the emitted HTTP request. Tests also cover direct resource construction, shared configuration, resource caching, named arguments, documentation retention and rejected method names. They do not validate live account permissions, provider settings or individual templates.

The fourth constructor argument accepts an injected transport for testing: callable(string $method, string $url, array $headers, ?string $body, int $timeout): array. It must return ['status' => int, 'body' => string, 'headers' => array]; response header names, when supplied, must be lowercase.

To update the reference, download the public collection to a local file and run node tools/import-reference.mjs collection.json, then run the tests again. The reference contains only operation names, methods and paths, not credentials or a full copy of the provider documentation.