mailjet/mailjet-apiv3-php
Official PHP wrapper for the Mailjet API v3. Send transactional email and SMS, manage contacts and campaigns through a simple, PSR-18 compatible client. Requires PHP 8.1+ and installs easily via Composer.
Architecture fit
mailjet/mailjet-apiv3-php) with no ServiceProvider, no facade, no publishable config, and no Laravel version constraints. Treat it as a vendor transport library, not a drop-in Laravel integration. Anything that looks like "Laravel integration" (config binding, credential resolution, queueing, event dispatch) is code we own.Client + Resources constants + get/post/put/delete returning a Response value object (success(), getStatus(), getData(), getReasonPhrase()). There is no domain model, no repository abstraction, no DTO layer. That is actually favorable for architecture: the surface area is small enough to hide behind our own port interface without fighting opinionated internals.php-http/discovery auto-wiring. PSR-18 injection is the single most valuable property here, because it lets us decorate the transport (retry, logging, circuit breaking, metrics, test doubles) without patching vendor code.Integration feasibility
composer require mailjet/mailjet-apiv3-php); Composer auto-installs a compatible PSR-18 client if none exists. In a Laravel app we already have Guzzle, so the discovery path is a no-op.MailjetTransport adapter + tests; +3–5 days if the scope includes contact/list sync, webhook ingestion, and per-message error reconciliation.$CALL = false) only suppresses outbound calls; it does not produce a realistic fixture response. Proper testing requires injecting a mock PSR-18 client (or Laravel's Http::fake() layered under Guzzle). Budget for building an internal fake rather than relying on $CALL=false.Technical risk
success() reflects transport/HTTP success, not delivery acceptance. Any code that gates on success() alone will drop mail without alerting. This is a correctness bug waiting to happen and must be addressed in the adapter, not left to feature teams.version 2.0.0 badge and cannot be true as of assessment. Either the registry data is wrong or the artifact is not what it claims. Verify the actual tagged release and its provenance before approving, and pin an explicit version rather than floating.options['url'] (api.mailjet.com vs api.us.mailjet.com). A misconfiguration routes PII to the wrong jurisdiction with no runtime guard. This needs to be a validated config value, not a free-form string.SSL_ERROR_SYSCALL and prescribes forcing IPv4. That is a known operational tax, not an edge case.Key questions
symfony/mailjet-mailer via Laravel's Mail manager may be the cheaper path and this SDK unnecessary.Stack fit
Symfony\Component\HttpClient\Psr18Client explicitly plus nyholm/psr7 rather than relying on discovery.config/services.php (consistent with Laravel's existing Mailgun/Postmark/SES conventions) rather than env() calls scattered at call sites.// config/services.php
'mailjet' => [
'key' => env('MAILJET_APIKEY_PUBLIC'),
'secret' => env('MAILJET_APIKEY_PRIVATE'),
'version' => 'v3.1',
'url' => env('MAILJET_API_URL', 'api.mailjet.com'),
'timeout' => 10,
],
// AppServiceProvider::register()
$this->app->singleton(MailjetClientFactory::class, fn ($app) => new MailjetClientFactory(
$app['config']->get('services.mailjet'),
$app->make(\GuzzleHttp\ClientInterface::class),
));
\Mailjet\Client directly into application code. Define our own narrow port (TransactionalMailer::send(Message $m): SendResult) and keep the SDK behind it. Given 274 stars and 0 dependents, the SDK is a dependency we must be able to replace in a day; a leaked vendor type turns a one-day swap into a two-week refactor.Migration path
MailjetBundle): the v2.0.0 PSR-18 migration changed the constructor and removed bundled HTTP coupling — audit for direct curl/Requests usage in legacy code, and check whether the old client set the v3.1 option (a silent default of v3 produces different payload semantics).Compatibility
version option is per-client, not per-call: v3 (email, default), v3.1 (send), v4 (SMS/contacts). A single misconfigured client silently uses v3 for sends — validate the version string at boot and fail fast.PUT behaves as PATCH (partial update semantics). Any code ported from a provider with strict full-replace PUT will produce surprising partial-state bugs.DELETE returns 204 with an empty body — do not assume a payload. SMS/v4 resources use ID where v3 uses id; casing inconsistency across resources is a real source of defects.Sequencing
composer.json.Maintenance
Support
v3/v3.1 default are predictable, recurring "why is this behaving oddly" questions — put both in the runbook up front.Scaling
Client per message is cheap but constructing a Guzzle client per message is not, and it defeats connection reuse.Failure modes
| Failure | Detection | Blast radius | Mitigation |
|---|---|---|---|
| v3.1 HTTP 200 with per-message errors | Only by parsing response body | Silent non-delivery | Reconcile per-message status; alert on any error entry; never gate on success() alone |
Timeout / SSL_ERROR_SYSCALL |
Exception or empty response | Failed send, possible duplicate on retry | Force-IPv4 Guzzle config; bounded retries; dedupe keys |
| 429 rate limit | Status code only | Throttled queue | Rate-limited queue worker; backoff on 429 |
version misconfiguration (v3 vs v3.1) |
Subtle wrong payload semantics | Malformed/rejected sends | Boot-time config validation |
| Wrong region endpoint | Undetected | PII to unintended jurisdiction | Allow-listed config enum, not free string |
| Credentials in logs | Undetected | Security incident | Redaction in Guzzle middleware; never log constructor args |
| Duplicate send on retry | Recipient complaint | Trust/reputation damage | Outbox/dedupe pattern or accept-and-document |
| Vendor outage | 5xx / connection errors | No transactional mail | Queued retry with backoff; secondary-provider failover only if explicitly built |
Ramp-up
Resources constant map, and a Response object. Comprehension cost is genuinely small.PUT-as-PATCH, inconsistent id/ID casing, filter/pagination syntax, and the v3.1 partial-failure model. Document these internally; the README will not.How can I help you explore Laravel packages today?