Download the PHP package funnelchat/funnelchat-php without Composer

On this page you can find all versions of the php package funnelchat/funnelchat-php. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package funnelchat-php

funnelchat-php

The official PHP SDK for the Funnelchat public API: contacts, conversations and messages, templates, WhatsApp forms, tags, custom fields, groups, numbers and webhooks, with the resources and their types generated from the OpenAPI spec.

Status: v0. The public API is at v0 and may change. The full reference is on the developers portal. Some types come from the code of the API in production rather than from the published spec: they are listed in Pending of the spec.

Installation

Guzzle is not required: any PSR-18 client with PSR-17 factories works (for example symfony/http-client with nyholm/psr7). The SDK finds them with php-http/discovery; if Composer asks about its plugin, you can say no.

The package is published on Packagist.

First call

You need an API key (<id>|fc_live_...) created in the panel, in "Developers" → "Create API key", with the permissions for what you are going to use. The panel groups them by area, one box per permission. There are twelve and a key can have all of them; there is no wildcard:

Area Permissions
Contacts and conversations View, Edit, Send
Groups and campaigns View, Edit, Send, Manage groups
WhatsApp numbers View, Edit, Create, connect and delete
Notifications (webhooks) View, Manage

This README names each one as the panel does: WhatsApp numbers → View is the View box of the WhatsApp numbers area.

Within each area, Send includes Edit and Edit includes View; Manage groups and Create, connect and delete are separate. Keep the key in an environment variable: the client reads it from FUNNELCHAT_API_KEY when you do not pass it.

Client options (named arguments):

Option What it is Default
apiKey Your key. FUNNELCHAT_API_KEY
environment production (https://api.funnelchat.com) or staging (https://api-staging.funnelchat.com). production
baseUrl Another base URL; it wins over environment (for example, the sandbox). —
timeout Maximum seconds of each attempt. Guzzle only (see below). 30 with the Guzzle the SDK builds
maxRetries Automatic retries (see Retries). 0 turns them off. 2
defaultHeaders Headers sent with every request. —
httpClient Your PSR-18 client. Guzzle if installed; otherwise the one discovery finds
requestFactory, streamFactory Your PSR-17 factories. the ones discovery finds
sleep How to wait between retries: function (int $milliseconds): void. Useful in tests. usleep

The timeout. PSR-18 has no timeout, so the SDK can only apply one with Guzzle: the Guzzle it builds by itself gets 30 seconds, and if you pass your own, it applies the timeout you set (if you set none, your Guzzle configuration rules). With another client, configure the timeout in that client: if you pass timeout, the SDK fails right away so nobody believes there is one.

Every request sends Authorization: Bearer <key>, Accept: application/json and User-Agent: funnelchat-php/<version> php/<version>. The key never shows up in a var_dump, print_r, var_export, dd() or serialize of the client, nor in the message of an error. A connection error carries your HTTP client's exception as getPrevious(), which may include the request with its headers: do not log it whole.

In Laravel, register a single client for the whole app: see examples/laravel/app/Providers/FunnelchatServiceProvider.php.

The resources

Resource What it has
$funnelchat->contacts list, create, upsert, retrieve, update, delete, the tags and fields of a contact, metaWindow (the 24-hour window), notes.
$funnelchat->conversations list, update, counters, mark everything as read or unread.
$funnelchat->messages send, sendDirect (the contact in the body), sendForm (a form), get (one message by id), list (a contact's history), sendContactCard, forward, react, removeReaction.
$funnelchat->templates list, create, retrieve, update, quickReplyButtons, send, delete.
$funnelchat->forms WhatsApp forms: list, templates, create, retrieve, update, delete, createVersion, sync, publish, numbers, addNumber, removeNumber, retryNumbers, definition, versions, listResponses, retrieveResponse and metrics.
$funnelchat->tags list, create, retrieve, update, delete, listContacts.
$funnelchat->customFields list, create, retrieve, update, delete.
$funnelchat->groups Read only: list, retrieve, listByDevice, metadata, listParticipants, listParticipantEvents, listMessages, listScheduledMessages and listActions.
$funnelchat->devices list (filter by external_reference), create (a QR number), qr, retrieve (with capabilities), update, delete, status, health, me, checkPhone, connection history. See Creating and connecting numbers.
$funnelchat->connectLinks create, retrieve, list, revoke: connect links for your customer to connect a number.
$funnelchat->webhooks Subscriptions (list, create, retrieve, update, delete, rotateSecret, test, listDeliveries), the catalog (events), verify and constructEvent.

The resources the Node SDK also has have the same methods, with the arguments in the same order: path parameters first, then the query or the body (arrays), and the call options last.

The resource classes are generated from the spec: the parameters, query, body and response of each method are typed in its PHPDoc with array shapes (array{id: int, name: string, …}). The schemas of the spec are PHPStan aliases in Funnelchat\Generated\Schemas (ContactResource, TagResource…), which you can use in your code with @phpstan-import-type ContactResource from \Funnelchat\Generated\Schemas. For any other route, see Any route.

Sending a message

Sending with an API key needs the Contacts and conversations → Send permission and follows the WhatsApp rules.

Open window: free text. With an official Meta number you can only send free text if the contact wrote to you in the last 24 hours. Check the window first:

A file by URL. With file (the URL) you send an image, a video, an audio, a document or a sticker. If the URL does not show the format (a signed URL, or one without an extension), say what it is with file_type (image, video, audio, document or sticker); for such a document, also file_name with its extension:

If one is missing, the API answers 422 with the field in errors (file_type, file_name or file), or 415 (UnsupportedMediaTypeException, with supportedFileTypes) when the extension cannot be sent.

Closed window: an approved template. The route takes the number (3) and the template id (45), which comes from $funnelchat->templates->list():

A WhatsApp form. form_id is the id of the published form (family_id); the answers arrive through whatsapp_form.completed:

When a send is stopped. If a send breaks a WhatsApp rule, it does not go out: you get an exception.

Error What happened What to do
422 window_closed Free text with the window closed, on an official number. Send an approved template.
422 antiblock_duplicate_content The same content repeated to the same contact. Change the text or wait.
422 antiblock_unanswered_limit Too many messages in a row to someone who does not answer. Wait until they answer.
422 recipient_mismatch The phone or whatsapp_id is not the one of the contact of the route. Use that contact's details.
422 recipient_unsupported The contact is a group, a broadcast list or a channel. Through the API you can only write to people.
422 device_unavailable The number is not connected to WhatsApp. Connect it and retry.
429 rate_limited You went over your number's pace. It always has retryAfter. Wait those seconds and retry.
429 mac_lockout_24h The account is in the 24-hour lockout for using up its MACs (also for a contact that is inactive by MAC). No retryAfter. Do not retry: wait for the lockout to end.

$exception->isSendRuleError() tells you whether the exception is one of these rules or one of the other refusals of a send (not_sent, template_not_found, form_not_published, provider_unsupported…; the list is in Funnelchat\Exception\SendErrors::CODES). A send is never retried by itself, because a retry can deliver the same message twice. If a 429 rate_limited sent nothing and you want the SDK to retry for you, ask for it in that call (a mac_lockout_24h is never retried):

The full example is in examples/plain-php/send-message.php.

Groups

Groups are read only through the API, with Groups and campaigns → View: the account's groups, their participants, joins and leaves, the messages sent to them and their scheduled messages.

Creating and connecting numbers

The official signup by link ('type' => 'meta') is not available in production yet: it answers 503 meta_connect_disabled until Funnelchat registers its domain with Meta. QR numbers and QR links work. Try the Meta flow against the sandbox, turning it on there.

A number belongs to the account of the API key and uses the plan's quota, with the same rules as the panel. Creating, deleting, asking for the QR and creating or revoking links need WhatsApp numbers → Create, connect and delete; reading links, WhatsApp numbers → View. Tag each number with your own external_reference (your customer's id, for example): it comes back in the number, in every device.* event, and filters the list.

A QR number

A connect link

A single-use page for your customer, with no Funnelchat session. The number is created when the page ends.

At most 20 links are open per account, and 10 can be created per minute.

Finding, tagging and deleting your customer's numbers

Deleting is physical and frees the quota: an official number loses the app subscription but stays registered with Meta, and a QR number's session is closed. It emits device.deleted. There is no undo.

devices->list() now takes the query first, like every other list; the call of 0.1, devices->list($options), still works.

Idempotency-Key

devices->create() and connectLinks->create() take idempotencyKey in the options (1 to 255 characters), sent as the Idempotency-Key header. For 24 hours, the same key with the same body answers the same number or link without creating another, with the header Idempotent-Replayed: true (read it in $funnelchat->lastResponse()->headers). Only successful answers are replayed: a refusal is evaluated again. With a different body, 422 idempotency_key_mismatch; while the first call runs, 409 operation_in_progress. With a key, the SDK also repeats the create after a timeout or a 5xx (see Retries).

The events of a number

Event data
device.connected device, connected_at, connect_link_id
device.disconnected device, disconnected_at, reason: qr_logged_out, meta_account_not_registered, shadow_ban_protection, provider_error, disconnected_by_user or unknown, connect_link_id
device.deleted device, deleted_at, connect_link_id
device.health_changed device, previous_state, state (available, limited, blocked or unknown), changed_at, connect_link_id

device is id, name, number, connection (qr, coexistence, dedicated or null) and external_reference. connect_link_id is, in device.connected, the link of that connection; in the other three, the last link that connected the number; null when no link did. The fields that production does not send yet (device.connection, device.external_reference, connect_link_id and reason) are optional in the types: read them with ?? null. The types are DeviceConnectedEvent, DeviceDisconnectedEvent, DeviceDeletedEvent and DeviceHealthChangedEvent in Funnelchat\Generated\WebhookEvents.

Receiving webhooks with Laravel

Verify the signature over the raw body, exactly as it arrived: $request->getContent(), not $request->all() or $request->json(). If you decode it and encode it again, the signature changes.

examples/laravel has it put together with a controller, a job that deduplicates by id and dispatches by type, the client's provider, the route and the configuration. We tested it copied as is into a Laravel 13 app (with PHPStan at the max level, through Larastan).

With plain PHP

The full example is in examples/plain-php/webhook.php.

What each function does

The event types. type is a string, because new types can arrive before the SDK knows them: answer 2xx to the ones you do not handle. Once you checked type, the alias of that event gives you the full shape (they are in Funnelchat\Generated\WebhookEvents: MessageReceivedEvent, MessageStatusChangedEvent…; and XxxData for its data):

The catalog of events the SDK knows, with their status, is Funnelchat\Generated\WebhookEvents::CATALOG.

Subscribing

Pagination

The list… methods return a lazy Paginator: nothing is requested until you iterate it.

The SDK understands the three ways the API paginates: by page number (meta.current_page and meta.last_page), by cursor (meta.next_cursor) and lists that come whole in an array. The page size is requested with each route's parameter (perPage, per_page or pageSize, as the spec says). $page->body is the full response of that page, with meta and links.

Errors

Everything the SDK throws extends Funnelchat\Exception\FunnelchatException. When the API answers with an error, it is an ApiException:

Property What it is
status The HTTP status (also getCode()).
errorCode The stable code of the body (window_closed, rate_limited, route_not_available, NUMBER_NOT_ELIGIBLE…), or null when there is none (for example a validation 422 or a 415). In Node it is code; in PHP getCode() already exists and returns a number.
getMessage() The message of the body, meant for people. Decide by status and errorCode, not by the message.
requestId The X-Request-Id of the response. Quote it when you report a problem, with the approximate time of the call.
retryAfter The seconds of Retry-After, when present (rounded up).
errors In a validation 422, the messages per field.
body, headers, method, url The response and the request (the URL without the query).

Each HTTP status has its class, in Funnelchat\Exception: BadRequestException (400), AuthenticationException (401), PermissionDeniedException (403), NotFoundException (404), ConflictException (409), UnsupportedMediaTypeException (415), UnprocessableEntityException (422), RateLimitException (429) and InternalServerException (5xx). Without a response: ApiConnectionException, and ApiConnectionTimeoutException when the timeout ran out.

The errors of the API entrypoint itself, which answers without reaching any area, have their own:

Class Error What to do
RouteNotFoundException (a NotFoundException) 404 route_not_found: the route does not exist. Check the route and that it starts with /api/v1/.
EdgeMisconfiguredException (an InternalServerException) 500 edge_misconfigured. It is on our side: report it quoting requestId.
OriginUnreachableException (an InternalServerException) 502 origin_unreachable: the area could not be reached. Treat it like a timeout: a read is repeated; a write, only after reading the state (see Retries).

A 403 with errorCode route_not_available means that route is not open to API keys, even if your key has the permission.

The refusals of creating or connecting a number are in Funnelchat\Exception\DeviceErrors, each with the statuses it comes with, and $exception->isDeviceError() tells them apart. The ones you will see most: 403 numbers_not_included, 403 number_limit_reached (with limit, used and pending_device_ids in body), 409 pending_number_exists (with device_id), 422 method_already_present (with device_id), 422 connection_type_mismatch, 422 idempotency_key_mismatch, 429 too_many_open_links and 503 meta_connect_disabled. Nothing was created.

The request id

Every response of the API, success or error, carries X-Request-Id: a different UUID per request, with which support finds the call. On an error it is in $exception->requestId; on a success, in $funnelchat->lastResponse() (the client's last response, with status, requestId and headers) or, for one call in particular, in onResponse, which also sees every page of a list:

An id you send yourself in X-Request-Id is not used: the API sets its own. There is no route to look a call up by its id.

Retries

Any route

request() calls any route, whether it is in a resource or not. Path parameters go in path, the query in query and the body in body:

It returns the decoded JSON (associative arrays), the text when the response is not JSON, or null when it came empty (204). Here the SDK does not know the shape of the response: if you want it typed, use the schema alias (/** @var BroadcastResource $broadcast */). The body [] is sent as {}.

paginate() is the same for lists: foreach ($funnelchat->paginate('/api/v1/broadcasts') as $broadcast).

The query is built the way Laravel reads it: arrays go as key[]=a&key[]=b, nested arrays with brackets, booleans as 1 and 0 and dates (DateTimeInterface) in ISO 8601, in UTC.

Funnelchat\Generated\Routes::ALL has every operation of the spec with its domain, its permission and the SDK method that calls it (sdk), or null when it is called with request().

Regenerating

Everything generated comes from a single script, in PHP:

What it does (scripts/generate.php):

  1. With --spec, it copies the spec to spec/funnelchat.json and writes spec/procedencia.json: repository, branch, commit, date and sha256 of the file.
  2. It adds spec/pending.json (the pending of the spec layer), but only what the snapshot does not bring. At the end it lists what the new snapshot already brings, so you can delete it from spec/pending.json.
  3. It writes src/Generated/Routes.php (every operation), src/Generated/Schemas.php (every schema as a PHPStan alias), src/Generated/WebhookEvents.php (the catalog and the shape of each event, taken from the schema or from the example), src/Resources/*.php (one method per operation, with its types) and src/Generated/ResourceProperties.php (the client's properties).

spec/resources.json says which operation each method of each resource is: they are the same names as in Node. Regenerating never needs code edited by hand. If an operation of that list is no longer in the spec, the generation fails and says which one; if its shape changed, composer analyse flags it. After regenerating: composer check.

The current snapshot is the portal's, branch feat/openapi-unificada @ 0ef4abb, of 15-10: 191 paths and 230 operations of conversations 9d723d52c, communities fab0bb10 and accounts c234b6f8 (13-10). It predates what reached production on 21-10: what is new is in the pending layer. The details are in spec/procedencia.json. The snapshot's own texts (summaries and descriptions of its operations and events) are in Spanish, as the portal publishes them.

Pending of the spec

The spec snapshot is from 13-10. What reached production afterwards is in a separate layer, marked as such in the code, which the next regeneration replaces by itself once a snapshot brings it (and it tells you which entries can be deleted):

All of it comes from the code of the domains at the tags running in production on 22-10 (read only, git show <tag>:<file>): conversations v1.18.10 (a580a3d0), communities v2.34.52 (6f32f0b4) and accounts v1.4.24 (300cc843), plus the API entrypoint, api-edge v0.3.0 (a1c5b350).

What Where it comes from
What the API-key sends answer (200 with the queued message, never 204), messages->get, client_reference (also in the history), file_type and file_name conversations v1.18.10
WhatsApp forms (/api/v1/forms…), sending one to a contact and whatsapp_form.completed conversations v1.18.10
The shape of message.status_changed (with error.reason), now live, and device_id in message.received conversations v1.18.10
The group.message_received, group.reaction_added and group_join_request.* events communities v2.34.52
Webhook subscriptions (/api/v1/webhooks…, with the Notifications (webhooks) permissions), the accounts events and four communities ones accounts v1.4.24
capabilities and the other new fields of each number in devices accounts v1.4.24
Creating, connecting and deleting numbers (devices->create, devices->qr, devices->delete's empty answer, the external_reference filter and field, connectLinks, Idempotency-Key, country) and the fields and events of device.*. In production since 25-10, except type: 'meta' (503 meta_connect_disabled) the contract 1022-alta-de-numeros v0.2 (23-10) and the changes of 25-10 (the QR every 5 seconds, accounts #1335; meta off; country), source accounts-number-onboarding; to be checked against accounts' OpenAPI

The error codes of the sends are in Funnelchat\Exception\SendErrors, written by hand from the same sources: the spec does not list them. Those of creating and connecting numbers, in Funnelchat\Exception\DeviceErrors, from the contract.

Development

The suite runs on PHP 8.1 (with PHPUnit 10) and on PHP 8.5 (with PHPUnit 12), with Guzzle 7 and with Guzzle 8.

Testing against the sandbox

funnelchat-sandbox imitates the API and sends signed webhooks, with no real effects. Point the client at it with baseUrl and any well-formed key:

The Meta sign-up by link starts off in the sandbox, like production: type: 'meta' answers 503 meta_connect_disabled. Turn it on to try it, with npx funnelchat-sandbox --meta-connect, or while it runs:

The SDK's end-to-end tests run against it, version 0.2.0 or later (a send and its signed notices, which a receiver on PHP's built-in server verifies; the refusals; messages->get; a form from build to answer; X-Request-Id; a QR number created, scanned, disconnected and deleted, a connect link completed on its page, country, the refusals and the four signed device.* events). The tests of a meta link turn the Meta sign-up on by themselves:

License

MIT.


All versions of funnelchat-php with dependencies

PHP Build Version
Package Version
Requires php Version ^8.1
ext-json Version *
php-http/discovery Version ^1.19
psr/http-client Version ^1.0
psr/http-factory Version ^1.0
psr/http-message Version ^1.1 || ^2.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package funnelchat/funnelchat-php contains the following files

Loading the files please wait ...