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 Meta Ads Laravel Package

jeffersongoncalves/laravel-meta-ads

Laravel Meta Ads offers a clean integration with the Meta Marketing API for managing Facebook and Instagram ad campaigns, ad sets, creatives, audiences, and performance insights directly from your Laravel application with a simple, fluent interface.

View on GitHub
Deep Wiki
Context7

Getting Started

This is the first release (1.0.0), so treat the public API as promising but not yet battle-tested — pin the version in composer.json ("^1.0") and read the changelog before minor bumps.

  1. Install and publish config

    composer require <vendor>/laravel-meta-ads
    php artisan vendor:publish --tag=meta-ads-config
    
  2. Set credentials in .env — you will need a Meta app ID/secret and a user or system-user access token with ads_read (and ads_management if you write):

    META_ADS_APP_ID=
    META_ADS_APP_SECRET=
    META_ADS_ACCESS_TOKEN=
    META_ADS_API_VERSION=v21.0
    
  3. Where to look first

    • config/meta-ads.php — API version, base URI, token, timeout, retry settings. This is the file you will touch most.
    • The service provider — confirms what is bound into the container and whether the client is a singleton (it almost certainly is).
    • The facade / root client class — the entry point for all resource calls.
    • The Resources/ (or equivalent) directory — one class per Graph object: accounts, campaigns, ad sets, ads, audiences.
  4. First use case: list ad accounts, then campaigns for one account.

    use MetaAds\Facades\MetaAds;
    
    $accounts = MetaAds::accounts()->all();          // /me/adaccounts
    $account  = $accounts->first();
    
    $campaigns = MetaAds::account($account->id)
        ->campaigns()
        ->fields(['id', 'name', 'status', 'objective', 'daily_budget'])
        ->all();
    

    If the facade name differs, resolve from the container instead: app(\Vendor\MetaAds\MetaAdsClient::class).

Implementation Patterns

Fluent resource chaining. The package models the Graph API as a root client → account scope → resource collection. Keep this shape in your own code; don't scatter raw Http::get('graph.facebook.com/...') calls alongside it, or you will end up with two auth/pagination/error strategies.

Wrap it in your own service. Rather than injecting the package client into controllers, put a thin MetaAdsService (or per-domain repositories like CampaignRepository) in front of it. This gives you one place to add caching, tenant token switching, and DTO mapping — and one place to patch if a 1.x minor release changes a method signature.

Map responses to DTOs early. Graph responses are associative arrays with inconsistent typing (budgets are strings in minor units, statuses are enums-as-strings). Convert at the boundary:

final class Campaign {
    public function __construct(
        public readonly string $id,
        public readonly string $name,
        public readonly string $status,
        public readonly int $dailyBudgetMinor,
    ) {}

    public static function fromGraph(array $row): self { /* ... */ }
}

Paginate with cursors, not offsets. Graph uses paging.cursors.after. If the package exposes an iterator/lazy collection, prefer it over all() for anything that could exceed a few hundred rows; all() on a large account will happily pull thousands of records into memory.

Sync jobs, not request-time calls. Ads data changes on Meta's schedule, not yours. Put pulls in queued jobs (SyncCampaignsJob, SyncInsightsJob) with WithoutOverlapping middleware, and write results to local tables. Your dashboards then read the database, not the Graph API.

Schedule::job(new SyncInsightsJob($accountId))
    ->hourly()
    ->withoutOverlapping();

Insights are the real workload. Campaign/ad-set/ad objects give you configuration; insights() gives you spend, impressions, CTR. Expect to pass fields, date_preset or time_range, level, and time_increment together, and to have one job per (account, level, date range) combination.

Multi-tenant / multi-account. If you serve several clients, do not rely on the single configured token. Resolve a per-tenant token (system user tokens are the sane choice for background jobs) and either instantiate the client per tenant or use whatever withToken()-style method the package provides. Cache the resolved client per tenant in the container.

Write operations need explicit guards. Creates and updates (status => 'PAUSED', budget changes) are real money. Route them through an action class that logs the payload, the actor, and the response ID, and gate them behind authorization. Meta's API will accept a budget change of 0 or a currency mismatch without much complaint.

Test with HTTP fakes. The package presumably uses Laravel's HTTP client underneath, so Http::fake() with fixture JSON is the fastest path. Assert on the outgoing URL to catch field-expansion and pagination regressions, and include fixtures for: a normal list, a paginated list, a permission error (code: 200), and a rate-limit error (code: 17 / 613).

Gotchas and Tips

Ad account IDs need the act_ prefix in URLs but often arrive without it. Normalize once, in your repository, and you will avoid a class of confusing (#100) Unsupported get request errors.

Graph returns errors with HTTP 200 in some cases, and always nests them under error. Don't rely solely on $response->successful(). Check for an error key and surface error.code, error_subcode, and error_user_msg — the last one is the only human-readable part.

Rate limiting is per-app-per-account and reported in the X-Business-Use-Case-Usage header, not just X-App-Usage. If the package exposes response headers, log them. A naive "retry 3 times with backoff" will make a throttled account worse; back off hard (minutes, not milliseconds) and let the next scheduled run pick up the work.

Short-lived tokens expire in ~60 days; user tokens in ~1–2 hours. For anything scheduled, use a system user token from Business Manager or implement token refresh. A silent token expiry looks like an auth error on every job at once — alert on it.

Field expansion is a string DSL, not an array of nested objects. fields=name,campaign{id,name} — if the package takes an array, it is almost certainly joining it, so don't pass ['campaign' => ['id']] and expect it to work. Verify the generated query string with Http::fake() and a dump.

Insights have attribution and timezone quirks. date_preset=last_30d is relative to the ad account's timezone, not your app's. Numbers pulled "today" will keep changing for hours. Store the date_start/date_stop Meta returns, not the range you asked for.

Deletion is soft. Archived/deleted campaigns may still appear in list responses unless you filter on effective_status. And requesting a deleted object by ID returns an error rather than a tombstone — handle that path explicitly.

Batch requests are the escape hatch for slow syncs. If the package supports batch(), use it for multi-account pulls; if not, keep the number of sequential calls per job modest and split across jobs rather than raising timeouts.

Version pinning matters more than usual. The Graph API version lives in the URL. Pin it in config and treat a version bump as a code change with a test run against a sandbox ad account — Meta does remove fields between versions.

Sandbox first. Do every write-path test against a real ad account in a Business Manager sandbox with a tiny budget, using Http::fake() only for unit tests. Graph API validation is shallow enough that mocks will happily accept payloads the live API rejects.

This is a 1.0.0. Read the source of the resource classes you depend on before building on top of them; extension points (custom endpoints, raw requests, middleware) are the thing most likely to shift in 1.x minors. If the package lacks a request()/raw() escape hatch, wrap the client so you can add one locally instead of forking.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums