mycomm / mycomm-php
Official PHP SDK for the MyComm messaging API: messages, contacts, conversations, channels, webhooks and the partner API.
Requires
- php: ^8.2
- ext-json: *
- php-http/discovery: ^1.20
- psr/http-client: ^1.0
- psr/http-client-implementation: *
- psr/http-factory: ^1.0
- psr/http-factory-implementation: *
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.9
- laravel/pint: ^1.24
- pestphp/pest: ^3.8 || ^4.0
- phpstan/phpstan: ^2.1
- symfony/yaml: ^7.0 || ^8.0
Suggests
- guzzlehttp/guzzle: Used automatically when installed, with the SDK's timeout applied.
- mycomm/mycomm-laravel: Service provider, facade and webhook route for Laravel.
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-11-02 16:23:41 UTC
README
The official PHP client for the MyComm messaging API: send and track messages, manage contacts and conversations, receive webhooks, and (for partners) give each of your users a line their own phone sends from: see Partners.
- Every operation of the public API and the partner API, with typed arguments and typed results
- Checks your arguments against the API's documented rules before sending, and tells you which field is wrong
- Retries that cannot double-send: every POST carries an
Idempotency-Key - Lists you can
foreachover, across pages - Webhook signature verification and a class per event
- A fake client for your own tests
Requires PHP 8.2 or later. Using Laravel? Install mycomm/mycomm-laravel
instead: it adds configuration, a facade and a webhook route on top of this package.
Install
composer require mycomm/mycomm-php
The SDK talks HTTP through any PSR-18 client. If your project has none yet:
composer require guzzlehttp/guzzle
Quick start
use MyComm\Client; $mycomm = new Client(getenv('MYCOMM_API_KEY')); // mc_live_... $message = $mycomm->messages->send( to: '+923001234567', body: 'Your code is 4821', ); echo $message->id; // "01923f80-5c6d-7e7f-8a90-a1b2c3d4e5f6" echo $message->status->value; // "queued" (an enum case, MessageStatus::Queued)
Create the key in the MyComm portal under Developers → API keys. Keep it on your server: never ship it in a browser or a mobile app.
The example uses a key limited to one line (a sending identity whose senders are your connected channels), so
the call names no sender: MyComm picks the line's healthy sender. A key that reaches several lines adds
line_id: '...'; channel_id or from pin one channel.
How the SDK maps to the API
Each resource of the API reference is a property of the client and
each operation a method. The reference names an operation with a verb in camelCase (sendMessage); the SDK groups
the operations by resource, so the same call reads messages->send():
| Operation id in the reference | Call |
|---|---|
sendMessage |
$mycomm->messages->send(...) |
upsertContactByIdentity |
$mycomm->contacts->upsertByIdentity(...) |
blockContactIdentity |
$mycomm->contacts->identities->block($contactId, $identityId) |
rotateWebhookEndpointSecret |
$mycomm->webhookEndpoints->rotateSecret($id) |
createPartnerLineLink |
$mycomm->partner->lines->links->create($lineId, purpose: 'connect') |
setLineSenders |
$mycomm->lines->setSenders($lineId, senders: [...]) |
handOverConversation |
$mycomm->conversations->handover($id, line_id: $lineId) |
generator/config.php holds the full table of operation ids.
Arguments are named like the API's fields (reference_id, send_after), so the reference and your editor's
autocompletion agree. Ids in the path come first; everything else is best passed by name.
Sending messages
use MyComm\Enums\MessageCategory; $message = $mycomm->messages->send( to: '+923001234567', body: 'Sale ends tonight. Reply STOP to opt out.', category: MessageCategory::Marketing, send_after: new DateTimeImmutable('today 18:00'), reference_id: 'campaign-7-user-991', metadata: ['user_id' => 991], ); foreach ($message->warnings as $warning) { logger()->notice($warning->message); }
Check what a send would do without sending it:
$preview = $mycomm->messages->send(to: '+923001234567', body: $text, dry_run: true); $preview->segments; // how many SMS parts the text needs $preview->hold_reason; // why it would wait, if it would
Send up to 1,000 messages in one call. Items are accepted or rejected one by one:
use MyComm\Inputs\SendBatchItem; $batch = $mycomm->messages->sendBatch( messages: [ new SendBatchItem(to: '+923001234567', body: 'Hi Ada'), new SendBatchItem(to: '+923331112222', body: 'Hi Grace'), ], defaults: ['category' => 'transactional'], ); foreach ($batch->results as $result) { if ($result->error !== null) { report("Message {$result->index} was rejected: {$result->error->code}"); } }
Follow a message afterwards with $mycomm->messages->show($id), or let MyComm tell you through
webhooks.
What the SDK checks before sending
The API documents limits and rules for what you send: ranges, lengths, which fields exclude each other, which
need another. The SDK checks them first, so a mistake fails at once with the field named, instead of as a
422 from the server:
use MyComm\Exceptions\InvalidArgumentException; try { $mycomm->messages->send(to: $number, body: $text, expires_in: 30); } catch (InvalidArgumentException $e) { echo $e->getMessage(); // expires_in must be between 60 and 604800 (got 30). $e->field; // "expires_in" $e->rule; // "OUT_OF_RANGE", named like the API's own validation codes $e->violations; // every problem, not only the first }
| Rule | Example |
|---|---|
| Ranges and lengths | expires_in 60 to 604800; reference_id 1 to 100 characters without spaces; limit 1 to 200 |
| Allowed values | category is transactional or marketing; lane is high, normal or bulk |
| Formats | ids are UUIDs |
| Sizes | metadata at most 50 keys, 2048 bytes as JSON, scalar values only |
| Exactly one of | to or contact_id |
| At least one of | content, body, media_ids or template_id |
| Needs another field | variables needs template_id; category is required when lane is bulk |
The rules are generated from the API's OpenAPI document, so they follow the API. Nothing is sent when one fails. Rules the SDK does not know about, such as an opt-out, are still answered by the server (see Errors).
If the API relaxes a rule before you have updated the SDK, turn the checks off with new Client(..., validate: false). Values the API may add later (the enum of a response, an unknown error code) are never rejected.
Verify your settings
verify() checks that a key works and, when you give it a number to test with, that a message to that number
would be accepted. It sends nothing: the second check is a dry run.
$result = $mycomm->verify(to: '+923001234567'); // add lineId: '...' when the key can use several lines if (! $result->ok()) { foreach ($result->problems() as $problem) { echo $problem, "\n"; // the API's own message, for example why a recipient is refused } }
A dry run does not know whether the phone is online or whether the carrier delivers; a hold it can predict is in
$result->holdReason. See the guide Verify your integration.
Lists and pagination
A list method returns one Page. Loop over it for the items of that page, or use all() to walk every page;
the next page is fetched only when the loop reaches it.
$page = $mycomm->contacts->list(group_id: $groupId, limit: 100); foreach ($page as $contact) { // the first 100 } foreach ($mycomm->contacts->all(group_id: $groupId) as $contact) { // every contact of the group }
$page->next_cursor, $page->hasMore() and $page->next() are there when you page by hand, and
$page->autoPaging() does what all() does for lists that are not named list ($mycomm->conversations->messages($id)->autoPaging()).
Leaving a field out or clearing it
An update changes only the fields you pass. Where a field can be cleared, null clears it:
$mycomm->contacts->update($id, first_name: 'Ada'); // email untouched $mycomm->contacts->update($id, first_name: 'Ada', email: null); // email cleared
Errors
Every error answer of the API becomes an exception. Branch on code() (or errorCode, the same value as text),
which is stable; the message says what happened and what to do, in English, and may change.
echo $e and $e->summary() give one line with the code, status, message, details and request id.
use MyComm\Enums\ErrorCode; use MyComm\Exceptions\ApiException; use MyComm\Exceptions\ConnectionException; use MyComm\Exceptions\ValidationException; try { $mycomm->messages->send(to: $number, body: $text, category: 'marketing'); } catch (ValidationException $e) { $e->fields(); // ['to' => [['code' => 'INVALID_NUMBER', 'message' => '...']]] } catch (ApiException $e) { if ($e->is(ErrorCode::RecipientOptedOut)) { // do not retry: the recipient unsubscribed } $e->status; // 422 $e->code(); // ErrorCode::RecipientOptedOut, or null for a code newer than this SDK $e->errorCode; // "RECIPIENT_OPTED_OUT" $e->details; // ['address' => '+923001234567', ...] $e->requestId; // quote this to support $e->docUrl(); // the page of the error reference that explains the code echo $e; // RECIPIENT_OPTED_OUT (422): The recipient ... details: {...} request_id: ... } catch (ConnectionException $e) { // MyComm could not be reached, even after the retries }
| Exception | When |
|---|---|
BadRequestException |
400: malformed request, invalid cursor |
InvalidHeaderException |
400 INVALID_HEADER (extends BadRequestException): details['header'] says which |
UnsupportedMediaTypeException |
415 (extends BadRequestException): the body is not in the format the endpoint takes |
AuthenticationException |
401: key missing, unknown, revoked or expired |
PermissionDeniedException |
403: ability, scope, IP allow-list, suspended account |
NotFoundException |
404 |
ConflictException |
409, for example REFERENCE_ID_EXISTS |
PayloadTooLargeException |
413 |
UnprocessableException |
422: a rule refuses the request |
ValidationException |
422 VALIDATION_FAILED (extends UnprocessableException) |
RateLimitException |
429, after the retries |
ServerException |
5xx, after the retries |
All of them extend ApiException, which extends MyCommException, the base of everything the SDK throws.
InvalidArgumentException also extends MyCommException, but it is not an ApiException: no request was made.
Retries and idempotency
The SDK retries a call twice (configurable) when the network fails, when MyComm answers 429 or 503
(waiting as long as Retry-After asks) and on other 5xx answers, with exponential backoff.
That is safe for sends because every POST carries an Idempotency-Key, and a retry repeats the same key:
MyComm then replays the first answer instead of sending a second message. Pass your own key to make a
whole operation of yours repeatable, for example across a queue job's attempts:
use MyComm\RequestOptions; $mycomm->messages->send( to: $order->phone, body: "Order {$order->number} has shipped.", requestOptions: new RequestOptions(idempotencyKey: "order-shipped-{$order->id}"), ); $message->meta()->idempotentReplayed; // true when this was a replay
Webhooks
Verify each delivery with the signing secret of your webhook endpoint, then handle the typed event. Always pass the raw request body.
use MyComm\Exceptions\WebhookSignatureException; use MyComm\Webhooks\Events\MessageDelivered; use MyComm\Webhooks\Events\MessageFailed; use MyComm\Webhooks\Events\MessageReceived; use MyComm\Webhooks\Webhook; $payload = file_get_contents('php://input'); try { $event = Webhook::constructEvent($payload, $_SERVER['HTTP_MYCOMM_SIGNATURE'] ?? null, $secret); } catch (WebhookSignatureException) { http_response_code(400); exit; } match (true) { $event instanceof MessageDelivered => markDelivered($event->data->message->id), $event instanceof MessageFailed => markFailed($event->data->message->id, $event->data->message->failure?->code), $event instanceof MessageReceived => handleReply($event->data->message->from, $event->data->message->body), default => null, // an event you do not handle, or one added to MyComm later }; http_response_code(204);
Good to know:
- Answer fast with any 2xx and do the work in a queue. MyComm waits 10 seconds, then retries.
- Deliveries can repeat and arrive out of order. De-duplicate on
$event->idand never move a message status backwards. - Rotating the secret: during the overlap MyComm signs with both secrets, so verification keeps working
whichever one you hold. To switch without a gap, pass both:
Webhook::constructEvent($payload, $header, [$new, $old]). - An event type this SDK version does not know arrives as
MyComm\Webhooks\UnknownEventwithdataas an array. Answer 2xx.
Create the endpoint in the portal or with the API. The secret is shown once:
$endpoint = $mycomm->webhookEndpoints->create( url: 'https://example.com/webhooks/mycomm', events: ['message.delivered', 'message.failed', 'message.received'], ); $endpoint->secret;
Partners
A partner key (mc_partner_...) gives each of your users (an ERP's tenant, a shop on your platform) a line: their
own phone sends your messages, and you never hold their MyComm login. The flow for an ERP:
use MyComm\Client; use MyComm\Enums\HostedLinkPurpose; $partner = new Client(getenv('MYCOMM_PARTNER_KEY')); // 1. Once per tenant: create its line and keep the id. $line = $partner->partner->lines->create( external_ref: 'erp-tenant-20418', // your own id for the tenant, unique among your live lines name: 'Acme Traders', user_number: '+923001234567', // optional: only pre-fills the number on the connect page ); $tenant->update(['mycomm_line_id' => $line->id]); // 2. When the tenant opens your messaging settings: mint a connect link for this visit. $link = $partner->partner->lines->links->create( $line->id, purpose: HostedLinkPurpose::Connect, return_url: 'https://erp.example.com/settings/messaging', allowed_origins: ['https://erp.example.com'], ); // Hand $link->url to the page and never log it: it is a bearer credential for this line, valid 30 minutes.
<!-- 3. Render the widget (@mycomm/connect) with that URL. --> <mycomm-connect url="<?= htmlspecialchars($link->url) ?>" purpose="connect"></mycomm-connect>
// 4. Send as the tenant: every /v1 call of this client carries `MyComm-Line`. $acme = $partner->forLine($tenant->mycomm_line_id); $acme->messages->send(to: '+923009876543', body: 'Your invoice INV-1042 is ready'); // 5. Know whether the line can send: poll its status... $status = $partner->partner->lines->status($line->id); $status->state; // LineState::Connected, NeedsAttention or NotConnected $status->needs_attention_reasons; // e.g. [LineNeedsAttentionReason::DeviceOffline]
...or take the line.* webhooks (line.connected, line.needs_attention, line.recovered, line.detached,
line.quota_low, line.quota_exhausted) on an endpoint created with partner_lines: true; see Webhooks.
A partner key reaches /v1 only through a line it made, and only a few routes: sending, messages, conversations and
lines->get() of that line. Any other /v1 route answers 403 PARTNER_ROUTE_NOT_ALLOWED, with or without a line; one
of those routes called without forLine() (or the constructor option lineId) answers 400 LINE_CONTEXT_REQUIRED.
/partner/v1 calls never carry the header. verify() works for a partner key too: it checks the key on the partner API
and dry-runs the send through the line ($partner->forLine($lineId)->verify(to: ...)).
| Call | |
|---|---|
partner->lines->list(), all(), create(), get(), update(), detach() |
Your lines; detach() ends the line and its open links |
partner->lines->status($id) |
Whether the line can send, and why not |
partner->lines->vouch($id, recovery_request_id: ...) |
Answer a line.recovery_requested webhook when you know the person |
partner->lines->links->create($id, purpose: ...) |
A hosted link: connect, add_channel, replace_device, manage or claim |
partner->links->get($id), revoke($id) |
Follow or end a link |
partner->sponsorshipDefaults->get(), put(...) |
The sponsorship terms new lines get |
partner->usage->get(from: ..., to: ...) |
Usage per line |
An account key mints the same links for its own lines with $mycomm->lines->links->create($lineId, purpose: ...) and
follows them with $mycomm->links->get($id) and revoke($id).
Uploading files
use MyComm\FileUpload; $media = $mycomm->media->upload('/path/to/photo.jpg'); $media = $mycomm->media->upload(FileUpload::fromString($bytes, 'photo.jpg', 'image/jpeg')); $mycomm->messages->send(to: '+923001234567', media_ids: [$media->id]);
Testing your code
MyCommFake gives you a real client that never touches the network. Every operation answers with the example
of the API reference until you say otherwise.
use MyComm\Http\Request; use MyComm\Testing\FakeResponse; use MyComm\Testing\MyCommFake; $fake = new MyCommFake; $notifier = new OrderNotifier($fake->client()); $fake->respond('messages.send', ['status' => 'queued']); // your values over the example $fake->respondOnce('messages.send', FakeResponse::error(422, 'RECIPIENT_OPTED_OUT')); $notifier->orderShipped($order); $fake->assertCalled('messages.send', fn (Request $request) => $request->json['to'] === $order->phone); $fake->assertNotCalled('messages.sendBatch');
FakeResponse also has page(), validationError(), noContent(), connectionError() and json().
To test your webhook receiver, MyComm\Testing\FakeWebhook::make('message.delivered', secret: $secret) gives
you a signed payload and its MyComm-Signature header.
Configuration
$mycomm = new Client( apiKey: getenv('MYCOMM_API_KEY'), baseUrl: 'https://mycomm.cogneer.tech', // without /v1 httpClient: $yourPsr18Client, // optional maxRetries: 2, timeout: 30.0, // seconds; applies to the built-in Guzzle client validate: true, // check arguments against the API's documented rules first );
When you pass your own HTTP client, set its timeout yourself: PSR-18 has no setting for it.
What every result carries
$message->meta()->requestId; // quote to support $message->meta()->rateLimitRemaining; // requests left in the window $message->raw(); // the JSON as MyComm sent it
MyComm adds fields and values without a new API version. The SDK is built for that: a field it does not know
yet is still in raw(), and an enum value it does not know arrives as a plain string instead of an enum case,
so $channel->status === ChannelStatus::Active keeps working and nothing throws.
Versioning
The SDK follows semantic versioning. It is generated from the same OpenAPI documents that the API is tested against; CHANGELOG.md lists what each release added.
Release status: 1.0.x is published on Packagist. 2.0.0 (lines replace inboxes, a breaking release) is not
published yet; until it is, composer require mycomm/mycomm-php installs 1.0.x.
Contributing
Most of src/ is generated. See docs/development.md before changing anything.
License
MIT. See LICENSE.