Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Instantly Laravel Package

jeffersongoncalves/laravel-instantly

Laravel integration for Instantly.ai that lets you manage cold email campaigns, leads, and accounts from your PHP app. Offers a simple, configurable API client with Laravel-friendly facades, service provider bindings, and published config.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin Laravel wrapper around the Instantly.ai v1 REST API, exposing five resource areas: campaigns, leads, accounts, analytics, and blocklist. Treat it as an HTTP client with Laravel ergonomics (config, facade, container binding) rather than an ORM — there are no Eloquent models, migrations, or persisted state.

Minimal setup:

composer require <vendor>/instantly-laravel
php artisan vendor:publish --tag="instantly-config"
INSTANTLY_API_KEY=your_key_here
# INSTANTLY_BASE_URL=https://api.instantly.ai/api/v1

First use case to validate the integration end-to-end is lead ingestion into an existing campaign. It exercises auth, a read (list campaigns to grab an ID) and a write (create a lead), so it proves the wiring before you build anything else:

use Instantly;

$campaigns = Instantly::campaigns()->all();          // confirm auth + base URL
$campaign  = Instantly::campaigns()->find($campaignId);

Instantly::leads()->create([
    'campaign'    => $campaignId,
    'email'       => '[email protected]',
    'first_name'  => 'Jane',
    'last_name'   => 'Doe',
    'company_name'=> 'Acme',
]);

Where to look first, in order:

  1. config/instantly.php — base URL, timeout, API key resolution. Confirm the key is read from config() and not env() directly (the latter breaks under config:cache).
  2. The resource classes under src/ — they are the source of truth for available methods; the README typically lags a release.
  3. Whether the package ships a facade/alias and a container binding. If it only ships a binding, inject InstantlyClient rather than resolving from the container inline.
  4. Whether responses are arrays or typed DTOs/Collections — this determines how much mapping code you write.

Method names vary between SDK releases; the safest first step is a quick php artisan tinker session against a non-production API key to enumerate what each resource exposes before designing around it.

Implementation Patterns

Wrap it, don't sprinkle it. The facade is convenient but couples your domain code to the SDK's array shapes. Put a small adapter in app/Services/Instantly/ that returns your own DTOs. When v1 changes (or you move to v2), you change one class instead of forty call sites.

final class InstantlyLeadSync
{
    public function __construct(private InstantlyClient $client) {}

    public function push(LeadData $lead, string $campaignId): void
    {
        $this->client->leads()->create([
            'campaign' => $campaignId,
            'email'    => $lead->email,
            ...$lead->toInstantlyPayload(),
        ]);
    }
}

Keep campaign IDs out of code. Store them in config/instantly.php (per environment) or in a campaigns table with a instantly_id column. The package has no local concept of campaigns, so you own that mapping — and you will need it for analytics correlation and reconciliation.

Queue every write. Lead creation and blocklist updates are network calls with real latency and real rate-limit exposure. Dispatch jobs:

ProcessInstantlyLead::dispatch($lead)->onQueue('outreach');

Use ->delay() or a rate-limited queue (Redis + RateLimited middleware, or a Bus::batch() with allowFailures()) for imports of more than a handful of records. A failed_jobs table plus retryUntil handling for 429s is cheaper than debugging a timed-out CSV import in a request cycle.

Bulk imports. If the API exposes bulk lead creation, prefer one bulk call over N single calls — rate limits are usually scoped per request. Chunk by ~100–500 records, hash the payload, and keep a local synced_at/payload_hash column so re-runs are idempotent on your side.

Pagination. Analytics and lead listings are paginated. Don't pull everything into memory to count records. Either use a lazy cursor/chunk()-style loop if the resource provides one, or loop pages explicitly with a hard page cap and log when you hit it, so a silent truncation never becomes a wrong dashboard number.

Analytics should be cached, not live. Dashboard widgets hitting Instantly on every page load is the most common way teams hit rate limits. Cache per (campaign, date range, granularity) for 5–15 minutes:

return Cache::remember(
    "instantly:analytics:{$campaignId}:{$from}:{$to}",
    now()->addMinutes(10),
    fn () => Instantly::analytics()->campaign($campaignId, $from, $to),
);

Blocklist as a pre-flight check. The blocklist resource is most valuable before you push a lead, not after a bounce. Either check on submit (acceptable for low volume, cache the result set briefly) or mirror the blocklist into a local table on a schedule and check locally — the latter is far cheaper and lets you join against it in SQL.

Testing. The SDK talks HTTP, so Http::fake() at the transport layer is the correct seam — but only if the package uses the Laravel HTTP client and doesn't swallow the fake. Assert on the outgoing request, not just the return value:

Http::fake(['api.instantly.ai/*' => Http::response(['id' => 'lead_123'], 200)]);

Instantly::leads()->create([...]);

Http::assertSent(fn ($req) => str_contains($req->url(), '/lead')
    && $req['campaign'] === 'camp_abc');

If the package wraps its own Guzzle client, you'll need a MockHandler injection or a fake at your adapter boundary instead. Decide which before you write tests across the codebase.

Integration tips. Register a typed config object via a service provider binding so tests can swap the API key and base URL trivially. Add a instantly log channel so you can trace request IDs from the SDK separately from app noise.

Gotchas and Tips

  • Config caching breaks env() calls. If the package's service provider or config file calls env('INSTANTLY_API_KEY') outside the config file, everything returns null after php artisan config:cache. Verify by running config:cache locally before deploying — this is the single most common "works on my machine" failure with SDK packages.
  • Timezone and date-range arguments in analytics. Marketing APIs are frequently UTC-only or campaign-timezone-aware, and off-by-one-day errors on dashboards are the usual symptom. Pin the assumption in a test with a fixed Carbon::setTestNow().
  • Rate limits are per-key, shared across all resources. Analytics polling and a lead import compete for the same quota. Separate them onto different queues, or accept that the import will slow down during dashboard hours. Handle 429 with exponential backoff plus jitter — not a fixed sleep.
  • Auth errors are ambiguous. A 401 from an expired or revoked API key and a 401 from a key lacking scope for a workspace look identical. Log the response body and request path together; it turns a 30-minute debugging session into a two-minute one.
  • Silent partial failures on bulk endpoints. Bulk lead creation commonly returns 200 with per-item errors in the body. Check for an errors/leads_uploaded-style key rather than trusting the status code — otherwise bad emails disappear without a trace.
  • Email normalization matters. Blocklist matching and lead dedupe are usually case-insensitive on the API side, but your local mirror will silently diverge if you store mixed-case addresses. Lowercase on write, everywhere.
  • No webhooks in 1.0.0. There is no push notification of campaign events, so anything reactive (lead replied, campaign status changed) requires polling. Budget for it: poll on a schedule with a watermark column, not on a fixed full re-fetch, or you'll hammer the API and still show stale data.
  • This is v1 of a v1-only SDK. It is a brand-new initial release with no upgrade history, so pin it with a conservative constraint (^1.0) and keep your adapter thin. Expect breaking changes inside the 1.x line as the upstream API evolves — the package cannot be more stable than the API it wraps.
  • Extension points are limited. There's no plugin/event layer in a package this size, so customization happens at two seams only: constructor injection of the HTTP client, and your adapter. Don't subclass the facade — it isn't designed for it. If you need retries/metrics/logging middleware beyond what's shipped, add it at the client-binding level in your own service provider.
  • Debugging checklist for a surprising response: dump config('instantly') after config:cache; call the endpoint with curl using the same key to confirm whether the problem is the package or the credentials; enable Laravel's HTTP client logging (Http::globalRequestMiddleware) to see the exact payload the package builds — field-name mismatches (campaign vs campaign_id) are the usual culprit on writes.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource