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

jeffersongoncalves/laravel-linkedin-ads

Laravel LinkedIn Ads by Jefferson Gonçalves integrates LinkedIn Advertising into Laravel apps, providing a simple API client and fluent methods to manage ad accounts, campaigns, creatives, and reporting directly from your PHP application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish the config, then set credentials in .env:

composer require <vendor>/laravel-linkedin-ads
php artisan vendor:publish --tag="linkedin-ads-config"
LINKEDIN_ADS_CLIENT_ID=
LINKEDIN_ADS_CLIENT_SECRET=
LINKEDIN_ADS_ACCESS_TOKEN=
LINKEDIN_ADS_API_VERSION=202401   # if exposed by the config

Where to look first: the package's service provider and config file — they tell you whether credentials are read once at boot (single-tenant) or injected per call (multi-tenant). That distinction drives almost every design decision downstream. Second stop: the src/Resources (or equivalent) directory, which mirrors the Marketing API surface one-to-one: adAccounts, campaignGroups, campaigns, creatives, analytics, audiences.

First use case — the smoke test: resolve the ad accounts the token can see, then pull one week of campaign metrics. If both work, your OAuth scopes (r_ads, rw_ads, r_ads_reporting) and account URNs are correct, which is 80% of the integration risk.

use LinkedinAds\Facades\LinkedinAds;

$accounts = LinkedinAds::adAccounts()->all();

foreach ($accounts as $account) {
    dump($account->id, $account->name, $account->status);
}

Method names below follow the package's resource-per-endpoint convention. Because this is a 1.0.0, verify signatures against the README before pasting into production — the request shapes (Rest.li, URNs, async reports) are dictated by LinkedIn and will not change.

Implementation Patterns

Thin resource wrappers, not an ORM. The package mirrors endpoints; it does not model relationships for you. Treat it as a typed HTTP client and keep your own persistence layer (synced campaigns, daily metrics tables) around it.

Campaign creation is two-step. LinkedIn requires a campaign group before a campaign:

$group = LinkedinAds::campaignGroups()->create([
    'account'     => $accountUrn,          // urn:li:sponsoredAccount:123456
    'name'        => 'Q3 Retargeting',
    'status'      => 'DRAFT',
]);

$campaign = LinkedinAds::campaigns()->create([
    'account'       => $accountUrn,
    'campaignGroup' => $group->id,
    'name'          => 'Web Visitors — 30d',
    'objectiveType' => 'WEBSITE_VISIT',
    'type'          => 'SPONSORED_UPDATES',
    'dailyBudget'   => ['amount' => '50.00', 'currencyCode' => 'USD'],
    'status'        => 'DRAFT',
]);

Always create as DRAFT, attach creatives, then flip to ACTIVE in a separate call. A half-built campaign that goes live with no creative burns budget.

Analytics are asynchronous — never inline in a request. LinkedIn's analytics finder requires a submit → poll → fetch cycle:

$report = LinkedinAds::analytics()->create([...]);   // returns an id immediately

// poll until COMPLETE — do this in a queued job
$result = LinkedinAds::analytics()->find($report->id);

Wrap this in a ShouldQueue job with ->release(30) backoff, and cap the polling attempts. Doing it in a controller will time out on any account with meaningful spend.

Scheduled sync into local tables. The pattern that pays for itself: a nightly job per account that pulls timeGranularity = DAILY metrics into linkedin_campaign_metrics, keyed on (account_id, campaign_id, date). Then dashboards, alerts, and joins against your own revenue data are instant and free of API quota.

Schedule::command('linkedin:sync-metrics')->dailyAt('03:00');

Multi-tenant: bind credentials per call, not per boot. If you serve multiple clients, resolve the token from your linkedin_integrations table and hand it to the client at runtime rather than mutating global config (which leaks between queued jobs on long-running workers). Cache ad-account lists for an hour — they change rarely and every page pays the quota cost.

Audience count estimation is a pre-flight check, not a reporting metric. Use it before saving a targeting spec so a user doesn't build an audience of 400 people, then launch a campaign that never delivers. Show the estimate as a range and gate submission on a sensible floor (LinkedIn's own minimum is ~300 members).

Testing: the package issues requests through Laravel's HTTP client, so Http::fake() with fixtures for the Rest.li response shapes is enough. Do not point tests at a real ad account.

Gotchas and Tips

API versioning will be the first thing to break. Marketing API versions sunset roughly a year after release, and the version is sent as a LinkedIn-Version: YYYYMM header. A 1.0.0 pins a version at release time; put a calendar reminder to bump it and watch for deprecation notices in the API changelog. If the version isn't configurable, that's your first fork/patch.

Rest.li conventions leak everywhere. IDs are URNs (urn:li:sponsoredAccount:...), pagination is (start:0,count:10) offset-based — not cursor-based, so results can shift between pages if the underlying data changes. Money fields arrive as decimal strings, not floats; cast carefully and don't sum with + on floats. Dates in analytics requests use {year, month, day} objects, not ISO strings.

Tokens expire silently at 60 days (refresh tokens at ~365). Persist expires_at, refresh proactively in a scheduled job, and surface a reconnect prompt in your UI. A stale token surfaces as a 401 mid-report, which is a miserable debugging experience at 2am.

Rate limits are per-account, per-day, and unevenly documented. Read X-RateLimit-* headers on every response and back off on 429. Batch GETs are capped (~20 IDs per request) and constrained by URL length — chunk them and use the package's batch helper if it provides one.

Creative upload is multi-step: initialize the upload → PUT bytes to the returned signed URL → reference the resulting URN. The signed URL is short-lived; don't queue the PUT for later. Uploads commonly fail on MIME/size limits that LinkedIn reports with a generic 400.

Campaigns are not live on creation. status: ACTIVE alone isn't enough — budget, schedule, targeting, and an approved creative are all required, and approval is asynchronous on LinkedIn's side. Poll reviewStatus rather than assuming success.

There is no real sandbox. Use a dedicated test ad account with a trivial budget. Any test that creates entities against production is a liability.

Timezones: reporting dates are evaluated in the ad account's timezone, which may not be UTC or your app's timezone. Store the account timezone alongside metrics or your daily rollups will drift by a day at month boundaries.

Audience estimates are ranges with confidence intervals, not exact counts — presenting them as a precise number in a UI erodes trust the moment a campaign under-delivers.

Extension points worth knowing: if the package exposes a configurable base URL and an HTTP middleware stack, you get logging, retry, and request-ID capture for free. Hook Http::globalRequestMiddleware to log the x-li-request-id header — LinkedIn support will ask for it on every ticket.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle