beziworld / laravel-getresponse
Laravel integration for GetResponse API v3: contacts with custom fields and tags, campaigns, newsletters, autoresponders, transactional e-mails, webhooks and callbacks as events, rate limits, MAX and multiple accounts.
Requires
- php: ^8.3
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/events: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/routing: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-10 20:35:27 UTC
README
Laravel integration for the GetResponse API v3: contacts with custom fields and tags, a subscribe helper and an Eloquent sync trait, campaigns (lists), autoresponders, newsletters, transactional e-mails, webhooks and legacy callbacks as Laravel events, rate limit handling, GetResponse MAX and multiple accounts.
Features
- Contacts: list with filters and pagination (
LazyCollectionover every page), find by e-mail, create (single and batch), update, delete, tags and custom fields upsert, activities. subscribe()helper: add or update a subscriber by e-mail with custom fields and tags given by name (missing tags are created, existing tags and fields are kept);unsubscribe().SyncsWithGetResponsetrait for Eloquent models, explicit or automatic (after save and delete).- Campaigns (lists), tags, custom fields, "From" addresses, autoresponders (read), newsletters (list, read, create, cancel, statistics).
- Transactional e-mails (add-on):
TransactionalEmailbuilder (direct content or template with lexpad, cc, bcc, attachments), list, details and statistics. - Webhooks route (single events and batches) with
ContactSubscribed,ContactUnsubscribed,ContactBounced,ContactRejected,MessageOpened,LinkClicked,ContactCustomFieldChanged,ContactEmailChangedandWebhookEventReceived; legacy callbacks route withCallbackReceived; callbacks configuration through the API. - Rate limits:
X-RateLimit-*headers exposed after every call, 429 handled withtimeToReset, reads retried, writes never. - API key or OAuth 2.0 token, GetResponse MAX (US and PL hosts,
X-Domain),X-Parent-Login,X-Time-Zone. - Multiple accounts as named connections plus runtime credentials for multi-tenant apps.
- No SDK dependency: Laravel HTTP client, so
Http::fake()works. PHP 8.3+, Laravel 12 and 13, PHPStan level 8.
Why this package
| Package | Status (Packagist, November 2026) |
|---|---|
getresponse/sdk-php-client (1.07M downloads) |
Official generic SDK for API v3, release 2.0.1 from 2023, no Laravel integration, no webhooks |
dangermark/laravel-getresponse (18k) |
0.1 from 2019 (last push 2020), Laravel 5 only, thin v3 wrapper returning raw objects |
baorv/l5-getresponse (10.7k) |
v1.0.0 from 2019, Laravel 5 only |
None of them supports current Laravel versions, webhooks, callbacks, rate limit handling, MAX headers or multiple accounts. This package adds typed responses, a subscriber sync workflow, events for every webhook type and safe retry rules.
Installation
composer require beziworld/laravel-getresponse
php artisan vendor:publish --tag=getresponse-config # optional
GETRESPONSE_API_KEY=your-api-key # https://app.getresponse.com/api (unused keys expire after 90 days) GETRESPONSE_CAMPAIGN_ID=V3J # default campaign (list) for contacts GETRESPONSE_WEBHOOK_SECRET=long-random-string # GetResponse MAX # GETRESPONSE_ENVIRONMENT=max-pl # retail (default), max-us, max-pl # GETRESPONSE_DOMAIN=newsletter.example.pl
php artisan getresponse:status checks the credentials and prints the remaining API quota.
Contacts and subscribers
use Beziworld\GetResponse\Facades\GetResponse; $result = GetResponse::subscribe( 'jane@example.com', 'Jane Doe', customFields: ['city' => 'Gdansk', 'pets' => ['cat', 'dog']], // by custom field name tags: ['customer', 'vip'], // by tag name, created when missing ); $result->created; // true for a new contact (created asynchronously, HTTP 202) $result->contactId; // ID of an existing contact GetResponse::unsubscribe('jane@example.com');
$contact = GetResponse::findContact('jane@example.com'); // exact match in the default campaign $contact->tagNames(); $contact->customField('city'); // ['Gdansk'] $page = GetResponse::contacts(['campaignId' => 'V3J', 'createdOn' => ['from' => now()->subWeek()]], perPage: 1000); $page->totalCount; GetResponse::allContacts(['campaignId' => 'V3J'])->each(fn ($contact) => ...); // every page GetResponse::createContact('john@example.com', 'V3J', ['name' => 'John', 'dayOfCycle' => 0]); GetResponse::createContacts([['email' => 'a@example.com'], ['email' => 'b@example.com']], 'V3J'); // batch GetResponse::updateContact($contact->contactId, ['name' => 'Jane Smith']); // tags and fields given here replace GetResponse::addTags($contact->contactId, ['hR']); // by tag ID, keeps the others GetResponse::setCustomFields($contact->contactId, ['kL6Nh' => 'Sopot']); // by field ID, keeps the others GetResponse::contactActivities($contact->contactId); GetResponse::deleteContact($contact->contactId);
Filters are passed as query[...]; sort, fields and additionalFlags are sent as they are, dates are formatted
as ISO 8601.
Eloquent sync
use Beziworld\GetResponse\Concerns\SyncsWithGetResponse; class User extends Model { use SyncsWithGetResponse; public function getResponseCustomFields(): array { return ['city' => $this->city, 'plan' => $this->plan]; } public function getResponseTags(): array { return $this->is_customer ? ['customer'] : []; } public function shouldSyncWithGetResponse(): bool { return $this->newsletter_consent; } } dispatch(fn () => $user->syncToGetResponse()); // recommended: queued $user->removeFromGetResponse();
Override getResponseEmail(), getResponseName(), getResponseCampaignId() and getResponseConnection() when needed.
With GETRESPONSE_SYNC_AUTO=true models sync after every save and are removed after delete; failures are reported,
not thrown.
Campaigns, tags, custom fields
GetResponse::campaigns()->items; GetResponse::findCampaign('my_list'); GetResponse::createCampaign('new_list', ['languageCode' => 'PL']); GetResponse::tags()->items; GetResponse::findTag('vip'); GetResponse::createTag('vip'); GetResponse::customFields()->items; GetResponse::createCustomField('pets', 'checkbox', 'checkbox', ['cat', 'dog']);
Autoresponders and newsletters
GetResponse::autoresponders(['campaignId' => 'V3J'])->items; GetResponse::autoresponder('Q'); GetResponse::newsletters(['status' => 'sent'])->items; GetResponse::newsletterStatistics('N1'); GetResponse::createNewsletter([...]); // documented body: subject, campaign, fromField, content, sendSettings GetResponse::cancelNewsletter('N1');
Transactional e-mails
Requires the Transactional Emails add-on. The sender is a verified "From" address (GetResponse::fromFields()).
use Beziworld\GetResponse\Messages\TransactionalEmail; $id = GetResponse::sendTransactional( TransactionalEmail::create('Your order #1001') ->from('V') // fromFieldId ->to('jane@example.com', 'Jane') ->html('<p>Thank you!</p>') ->plain('Thank you!') ->tag('vBd5') // optional tag ID for statistics ->attachData($pdf, 'invoice.pdf', 'application/pdf') ); GetResponse::sendTransactional(TransactionalEmail::create()->from('V')->to('jane@example.com') ->template('tpl1', ['name' => 'Jane'])); GetResponse::transactionalEmail($id); GetResponse::transactionalStatistics('day', ['timeFrame' => ['from' => now()->subWeek()]]);
Up to 8 recipients (to, cc and bcc) per message.
Webhooks and callbacks
-
Enable the routes and set a secret:
GETRESPONSE_WEBHOOKS_ENABLED=true GETRESPONSE_WEBHOOK_SECRET=long-random-string
Route Name Purpose POST /getresponse/webhookgetresponse.webhookWebhooks (single events or batches) GET|POST /getresponse/callbackgetresponse.callbackLegacy callbacks -
In GetResponse (Tools, Webhooks) create a webhook with the URL
https://your-app.com/getresponse/webhook?secret=long-random-string. GetResponse does not sign webhooks; its documentation recommends this secret query parameter, which the middleware compares in constant time with the secret of every connection. Callbacks can be enabled through the API:GetResponse::enableCallbacks(route('getresponse.callback', ['secret' => config('getresponse.connections.default.webhook_secret')]), ['subscribe', 'unsubscribe']);
-
Listen to the events:
use Beziworld\GetResponse\Events\ContactBounced; use Beziworld\GetResponse\Events\ContactUnsubscribed; Event::listen(fn (ContactUnsubscribed $event) => User::where('email', $event->event->email)->update(['newsletter_consent' => false])); Event::listen(fn (ContactBounced $event) => User::where('email', $event->event->email)->update(['email_bounced_at' => now()]));
WebhookEvent exposes type (WebhookEventType), contactId, email, name, campaignId, campaignName,
accountId, messageId, messageSubject, linkUrl, reason, occurredAt and the raw payload. The response is
always {"status": "OK"}, without which GetResponse retries; it waits 5 seconds, so queue slow listeners. Callbacks
(4 second timeout, never repeated) dispatch CallbackReceived with the documented parameters.
Rate limits and retries
GetResponse::contacts(); GetResponse::rateLimit()?->remaining; // X-RateLimit-Remaining GetResponse::rateLimit()?->resetInSeconds; // X-RateLimit-Reset
The API allows 30 000 calls per 10 minutes and 80 per second. Reads are retried on connection errors and HTTP 5xx and,
after HTTP 429, once the frame resets when that happens within GETRESPONSE_MAX_RETRY_AFTER seconds (default 60).
Writes are never retried automatically; a rejected write throws ApiException with retryAfter().
Multiple accounts and GetResponse MAX
'connections' => [ 'default' => ['api_key' => env('GETRESPONSE_API_KEY'), 'campaign_id' => env('GETRESPONSE_CAMPAIGN_ID')], 'max' => ['api_key' => env('GETRESPONSE_MAX_API_KEY'), 'environment' => 'max-pl', 'domain' => 'newsletter.example.pl'], ],
GetResponse::connection('max')->subscribe('jane@example.com'); GetResponse::using(['api_key' => decrypt($tenant->gr_key), 'campaign_id' => $tenant->gr_list]); GetResponse::using(['access_token' => $oauthToken]); // OAuth 2.0 (Authorization: Bearer) GetResponse::resolveWebhookSecretsUsing(fn () => Tenant::pluck('gr_webhook_secret', 'id')->all());
Error handling
All exceptions extend Beziworld\GetResponse\Exceptions\GetResponseException:
| Exception | When |
|---|---|
ApiException |
GetResponse rejected the request (errorCode() such as 1008, 1014, 1015, httpStatus(), codeDescription(), context(), uuid(), moreInfo(), retryAfter(), isRateLimited(), isDuplicate(), isNotFound(), isAuthenticationError()) or could not be reached |
InvalidMessage |
transactional e-mail without sender, recipient, subject or content, too many recipients, unknown custom field name |
InvalidConfiguration |
missing credentials, unknown environment, MAX without domain, missing campaign, unknown connection |
Testing your application
Http::fake(['api.getresponse.com/v3/*' => Http::response([])]); $user->syncToGetResponse(); Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://api.getresponse.com/v3/contacts'));
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues and feature requests: GitHub issues
- Security vulnerabilities: see SECURITY.md
Need an integration? Get in touch through beziworld.eu.
License
MIT. See LICENSE.md. This package is not affiliated with, endorsed by or sponsored by GetResponse S.A. "GetResponse" is a trademark of its owner.