Looking to hire Laravel developers? Try LaraJobs

laravel-getresponse maintained by beziworld

Description
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.
Last update
2026/11/10 21:35 (dev-master)
License
Downloads
0

Comments
comments powered by Disqus

Laravel GetResponse

tests Latest Version License

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.

Polska wersja dokumentacji

Features

  • Contacts: list with filters and pagination (LazyCollection over 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().
  • SyncsWithGetResponse trait 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): TransactionalEmail builder (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, ContactEmailChanged and WebhookEventReceived; legacy callbacks route with CallbackReceived; callbacks configuration through the API.
  • Rate limits: X-RateLimit-* headers exposed after every call, 429 handled with timeToReset, 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

  1. Enable the routes and set a secret:

    GETRESPONSE_WEBHOOKS_ENABLED=true
    GETRESPONSE_WEBHOOK_SECRET=long-random-string
    
    Route Name Purpose
    POST /getresponse/webhook getresponse.webhook Webhooks (single events or batches)
    GET|POST /getresponse/callback getresponse.callback Legacy callbacks
  2. 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']);
    
  3. 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.

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.