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

Bingads Laravel Package

microsoft/bingads

The official Bing Ads PHP SDK provides proxy classes for all Bing Ads API web services, handling OAuth authentication and low-level SOAP details so you can build Bing Ads applications in PHP. Install via Composer as microsoft/bingads, autoload with PSR-4, and manage campaigns, ads, and reports progr...

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation

    composer require microsoft/bingads:^13.0.30.1
    

    Ensure extension=soap is enabled in php.ini. The 13.0.30.1 SDK targets API revision 2026-10-12 — older proxy versions will not expose the new AI/media/exclusion members described below.

  2. First Authentication Use the OAuthDesktopMobileAuthCodeGrant for local development:

    use Microsoft\BingAds\V13\Authentication\OAuthDesktopMobileAuthCodeGrant;
    use Microsoft\BingAds\V13\Authentication\AuthenticationHelper;
    
    $authenticationHelper = new AuthenticationHelper();
    $authCodeGrant = new OAuthDesktopMobileAuthCodeGrant(
        $authenticationHelper->GetDeveloperToken(),
        $authenticationHelper->GetRefreshToken(),
        $authenticationHelper->GetAccessToken(),
        $authenticationHelper->GetAuthCode()
    );
    $authCodeGrant->Authenticate();
    
  3. First API Call Verify the SDK surface you actually got before writing feature code:

    use Microsoft\BingAds\V13\CampaignManagement\CampaignManagementServiceClient;
    
    $serviceClient = new CampaignManagementServiceClient(
        $authCodeGrant->GetAccessToken(),
        $authCodeGrant->GetRefreshToken(),
        $authCodeGrant->GetCustomerId()
    );
    
    // Confirms CustomerId/CustomerAccountId headers are wired correctly.
    $accounts = $serviceClient->GetCustomerAccounts();
    
  4. First "new-in-13.0.30.1" Use Case The lowest-risk way to adopt the release is a media update loop: upload media with AI content labels, then swap creative in place with UpdateMedia instead of delete + re-add (which resets performance history and ad IDs).

    $media = $serviceClient->AddMedia([$imageMedia])->Media;
    $imageMedia->Id = $media[0]->Id;
    $imageMedia->AiContentLabel = 'Generated';   // confirm enum name in your proxy
    $serviceClient->UpdateMedia([$imageMedia]);
    

Key Resources

  • Official PHP SDK Guide
  • API Release Notes — the 2026-10-12 revision is the authoritative source for exact member names
  • vendor/microsoft/bingads/src/V13/CampaignManagement/ — inspect the generated proxies for UpdateMedia, AI content label fields, AI prompt audience and campaign term exclusion entities
  • vendor/microsoft/bingads/src/V13/Bulk/ — inspect new bulk column mappings (parent customer number / related account)

Implementation Patterns

1. Service Client Workflow

Pattern: Initialize once per request, but always read credentials from config — not from the constructor arguments of the auth grant.

// app/Services/BingAdsService.php
class BingAdsService {
    private CampaignManagementServiceClient $serviceClient;

    public function __construct(OAuthDesktopMobileAuthCodeGrant $auth) {
        $this->serviceClient = new CampaignManagementServiceClient(
            $auth->GetAccessToken(),
            $auth->GetRefreshToken(),
            $auth->GetCustomerId()
        );
        $this->serviceClient->SetCustomerAccountId(config('bingads.account_id'));
    }

    public function client(): CampaignManagementServiceClient {
        return $this->serviceClient;
    }
}

2. AI Content Labels & Detection Metadata (new in 13.0.30.1)

Pattern: Treat disclosure as a first-class attribute of every uploaded asset, set at creation time, and persist it locally so you can audit and re-assert it on update.

class MediaIngestService
{
    public function upload(UploadedFile $file, bool $aiGenerated, array $detection = []): int
    {
        $media = new Image();
        $media->Type = 'Image';
        $media->Data = base64_encode(file_get_contents($file->path()));

        // AI content label + detection metadata (verify exact property
        // names against your installed proxy before shipping).
        $media->AiContentLabel = $aiGenerated ? 'Generated' : 'None';
        $media->DetectionMetadata = json_encode($detection); // e.g. C2PA manifest digest

        $result = app(BingAdsService::class)->client()->AddMedia([$media]);

        if (!empty($result->PartialErrors)) {
            throw new MediaUploadException($result->PartialErrors[0]->Message);
        }

        return $result->Media[0]->Id;
    }
}

Store the label + metadata you sent in your own media_assets table — you cannot rely on round-tripping it back in every GetMediaMetaData response, and ad reviewers may ask.

3. UpdateMedia — In-Place Creative Replacement (new in 13.0.30.1)

Pattern: Media IDs are referenced by ads and asset groups. Prefer UpdateMedia over delete/re-add so downstream references stay valid.

public function replaceCreative(int $mediaId, UploadedFile $newFile): void
{
    $media = new Image();
    $media->Id = $mediaId;                  // ID is what makes it an update
    $media->Type = 'Image';
    $media->Data = base64_encode(file_get_contents($newFile->path()));

    $result = $this->client()->UpdateMedia([$media]);

    // UpdateMedia is per-item: partial failures do NOT throw.
    foreach ($result->PartialErrors ?? [] as $error) {
        Log::warning('Media update failed', [
            'media_id' => $mediaId,
            'code'     => $error->Code,
            'message'  => $error->Message,
        ]);
    }
}

Wrap this in a queued job — media validation (aspect ratio, file size, text-in-image) rejects individual items and you want retries with backoff, not a 500 in a request cycle.

4. AI Prompt Audiences

Pattern: Model AI prompt audiences as a normal audience with an extra payload, so existing audience-association code keeps working.

public function createAiPromptAudience(string $name, string $prompt, string $promptType = 'SearchTerm'): int
{
    $audience = new Audience();
    $audience->Name = $name;
    $audience->Type = 'AiPrompt';           // confirm constant in your proxy
    $audience->AiPrompt = $prompt;          // the natural-language seed
    $audience->AiPromptType = $promptType;  // e.g. intent / search term expansion

    $result = $this->client()->AddAudiences([$audience]);
    return $result->AudienceIds[0];
}

Keep prompts in a versioned config or DB row. Prompt-triggered audiences change delivery over time, so you want an audit trail of which prompt a campaign targeted in which month.

5. Campaign Term Exclusions

Pattern: Sync exclusions declaratively from a review queue; the entity is additive, so use a set-diff rather than blind re-adds.

public function syncTermExclusions(int $campaignId, array $terms): void
{
    $existing = $this->client()->GetCampaignTermExclusions($campaignId)->CampaignTermExclusions ?? [];
    $have = collect($existing)->pluck('Text')->map(fn ($t) => strtolower($t))->all();
    $want = collect($terms)->map(fn ($t) => strtolower(trim($t)))->unique();

    $toAdd = $want->diff($have)->values()->all();

    if ($toAdd) {
        $this->client()->AddCampaignTermExclusions($campaignId, $toAdd);
    }
}

Exclusions are campaign-scoped, so an account-wide "blocklist" must be fanned out — do that in a job with a per-campaign lock to avoid duplicate concurrent adds.

6. Bulk Operations

Use BulkService for large-scale updates:

$bulkService = new BulkServiceClient($auth);
$bulkOperation = new BulkOperationHeader();
$bulkOperation->Operation = BulkOperationType::Add;
$bulkOperation->Objects = [$campaign1, $campaign2];

$bulkResponse = $bulkService->ExecuteBulkOperation($bulkOperation);

New in 13.0.30.1: the bulk schema adds mappings for parent customer number and related account, letting you upload hierarchical account structures in one file rather than per-account API calls. Use these for agency/multi-account provisioning:

// Column set for the new mappings — verify names against the
// BulkServiceManager schema in your installed version.
$columns = [
    'Type', 'Status', 'Parent Customer Number', 'Related Account',
    'Account Name', 'Customer Id', 'Currency Code', 'Time Zone',
];

Keep the parent customer number as a string in your models — leading zeros and 8+ digit values will be truncated by an int cast.

7. Reporting

Generate and download reports:

$reportingService = new ReportingServiceClient($auth);
$reportRequest = new ReportRequest();
$reportRequest->ReportName = "CampaignPerformanceReport";
$reportRequest->Aggregation = ReportAggregation::Daily;
$reportRequest->Columns = ["CampaignName", "Impressions", "Clicks"];

$downloadRequest = new DownloadReportRequest($reportRequest);
$reportDownload = $reportingService->DownloadReport($downloadRequest);

If you report on creative or asset performance, confirm whether the 2026-10-12 revision adds AI-content-label columns to your report type before assuming you can segment by it — report columns lag feature releases.

8. Error Handling

try {
    $response = $serviceClient->GetCampaigns();
} catch (ApiException $e) {
    if ($e->getCode() === 401) {
        $auth->RefreshToken();
        // retry once via Laravel's retry() helper
    }
    throw $e;
}

Remember the split: service-level errors throw; partial errors (PartialErrors, BatchErrors) arrive in a 200 response and must be inspected per item. UpdateMedia, AddMedia and bulk uploads are all partial-error surfaces.

9. Configuration Management

BINGADS_DEVELOPER_TOKEN=your_dev_token
BINGADS_REFRESH_TOKEN=your_refresh_token
BINGADS_CUSTOMER_ID=1234567
BINGADS_CUSTOMER_ACCOUNT_ID=1234567
BINGADS_PARENT_CUSTOMER_NUMBER=00123456

Load via a dedicated config/bingads.php and cache it; do not call env() outside config files — it returns null once config:cache has run.


Gotchas and Tips

Upgrade-Specific Pitfalls (13.0.30.1)

  1. Generated proxies changed shape

    • The new release adds members to existing entities (Media, Audience, bulk records). If you subclass generated proxies (e.g. CustomCampaignServiceClient), re-check __construct signatures and any toArray/serializer overrides after the bump.
    • Tip: Pin the exact version and diff the proxy directory on upgrade:
      "microsoft/bingads": "13.0.30.1"
      
      git diff vendor/microsoft/bingads/src/V13/CampaignManagement/
      
  2. AI content labels are not retroactive

    • Media uploaded before you adopted labelling keeps its original state. If disclosure is a compliance requirement, re-assert labels through UpdateMedia for existing assets rather than assuming they inherit a label.
    • Tip: Backfill in a queued job keyed by your local media_assets table, and log the PartialErrors per media ID.
  3. UpdateMedia is not a "create-or-update"

    • Sending an Image without Id to UpdateMedia fails per-item; sending an unknown Id fails per-item. There is no upsert semantics.
    • Tip: Branch on your stored ID; call AddMedia when absent.
  4. Detection metadata must be serialized predictably

    • JSON-encoding a PHP array produces key ordering that may not match what you hashed or signed. For C2PA-style manifests, hash the canonical form, not json_encode() output.
  5. AI prompt audiences are volatile

    • Prompt-derived membership changes as the model re-evaluates; the audience size you read today is not the size you'll be billed against. Never cache audience size as a reporting figure.
  6. Campaign term exclusions are additive per campaign

    • Re-running a sync job without a set-diff produces duplicate-add partial errors on every run, polluting your logs and masking real failures.
  7. Parent customer number is a string

    • Casting to int in Eloquent silently breaks accounts with leading zeros, and the bulk mapping will fail with a non-obvious "account not found".

Authentication Pitfalls

  1. Token Expiry

    • Always handle 401 Unauthorized by refreshing the token:
      if ($e->getCode() === 401) {
          $auth->RefreshToken();
          retry(3, fn () => $serviceClient->GetCampaigns(), 200);
      }
      
  2. Sandbox vs. Production

    • Sandbox uses login.windows-ppe.net (not live-int.com as of v13.0.10).
    • Gotcha: Hardcoded URLs in older SDK versions may break; the 2026-10-12 revision moved the default endpoints again in some auth flows.
  3. Scopes

    • Default scope is msads.manage (required for MFA).
    • Override temporarily with oAuthScope in the ServiceClient constructor:
      $serviceClient = new CampaignManagementServiceClient(
          $auth->GetAccessToken(),
          $auth->GetRefreshToken(),
          $auth->GetCustomerId(),
          ['oAuthScope' => 'https://bingads.microsoft.com/Api/Advertiser/Manage']
      );
      

API-Specific Quirks

  1. Bulk Operations

    • Gotcha: Bulk operations fail silently on invalid objects. Validate before submission.
    • Tip: Use ValidateBulkOperation to pre-check:
      $validation = $bulkService->ValidateBulkOperation($bulkOperation);
      if ($validation->HasErrors) {
          throw new \RuntimeException("Bulk operation validation failed");
      }
      
    • New: hierarchical account uploads using parent customer number / related account fail the whole row when the parent isn't resolvable — validate parent references first.
  2. Reporting Delays

    • Some reports (e.g., OfflineConversionReport) take 24+ hours to populate.
    • Tip: Add a reportReady() check with exponential backoff.
  3. SOAP Headers

    • Gotcha: Missing CustomerId or CustomerAccountId in headers causes 400 Bad Request.
    • Tip: Verify headers with:
      $serviceClient->GetServiceClient()->GetSoapClient()->__getLastRequestHeaders();
      
  4. Rate Limiting

    • Bing Ads enforces 5 requests/second per endpoint.
    • Tip: Use Laravel's throttle middleware or a queue:
      Queue::later(now()->addSeconds(1), fn () => $serviceClient->GetCampaigns());
      
    • Media ingestion jobs are the easiest way to breach this — batch AddMedia calls and throttle the queue worker.

Debugging Tips

  1. Enable SOAP Debugging

    $soap = $serviceClient->GetServiceClient()->GetSoapClient();
    $soap->setDebug(true);
    $lastRequest  = $soap->__getLastRequest();
    $lastResponse = $soap->__getLastResponse();
    

    UpdateMedia payloads contain base64 image bytes — truncate before logging.

  2. Log Partial Errors, Not Just Exceptions

    foreach ($result->PartialErrors ?? [] as $error) {
        Log::warning('BingAds partial error', [
            'code' => $error->Code,
            'message' => $error->Message,
        ]);
    }
    
  3. Log Authentication Tokens — Don't

    • Security Note: Never log access_token or refresh_token in production.
    • Tip: Log only the customer_id and timestamp for debugging:
      Log::debug("BingAds request for customer {$auth->GetCustomerId()}");
      
  4. Version Mismatches

    • Gotcha: Using an outdated SDK produces InvalidOperationException on the new members, or silently drops them from the serialized SOAP body — which is worse, because the call still "succeeds".

Extension Points

  1. Custom Proxies

    • Extend auto-generated proxies (e.g., CampaignManagementServiceClient) to add business logic:
      class CustomCampaignServiceClient extends CampaignManagementServiceClient {
          public function getActiveCampaigns() {
              $campaigns = parent::GetCampaigns();
              return array_filter($campaigns, fn ($c) => $c->Status == 'Active');
          }
      }
      
    • After any SDK bump, run your test suite against these subclasses first — that's where signature drift surfaces.
  2. Media Asset Registry

    • Keep a local media_assets table (media_id, ai_content_label, detection_metadata, uploaded_at, campaign_id). It's the only reliable place to answer "which creatives are AI-generated?" and to drive UpdateMedia backfills.
  3. Event Listeners

    • Hook into Laravel's queue events to retry failed Bing Ads jobs:
      Queue::failing(function (JobFailed $event) {
          if (str_contains($event->exception->getMessage(), '401')) {
              $event->job->release(60);
          }
      });
      
  4. Model Bindings

    • Use Laravel's Eloquent to map Bing Ads entities:
      class BingAdCampaign extends Model {
          protected $fillable = ['name', 'status', 'budget'];
      
          public static function syncFromApi() {
              $campaigns = app(BingAdsService::class)->client()->GetCampaigns();
              return self::upsert($campaigns->toArray(), ['id']);
          }
      }
      
    • Add a campaign_term_exclusions pivot and an ai_prompt_audiences table alongside it; both are campaign-attached and easy to sync in the same job.
  5. Testing

    • Mock the ServiceClient in PHPUnit:
      $mockClient = $this->createMock(CampaignManagementServiceClient::class);
      $mockClient->method('GetCampaigns')->willReturn(new Campaign[]);
      $mockClient->method('UpdateMedia')->willReturn(
          (object) ['PartialErrors' => []]
      );
      $this->app->instance(CampaignManagementServiceClient::class, $mockClient);
      
    • Add fixtures for PartialErrors and batch errors — the happy path is the least interesting case in this SDK.
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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log