Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,8 @@ $client->ads()->boostable($workspaceId);
$client->ads()->connections($workspaceId);
$client->ads()->sources($workspaceId); // ad accounts and Pages per connection

$url = $client->ads()->authorizeMeta($workspaceId); // finish the login in a browser
$client->ads()->providers(); // the networks this deployment knows
$url = $client->ads()->authorize('meta', $workspaceId); // finish the login in a browser
$client->ads()->deleteConnection($connectionId, $workspaceId);

$boost = $client->ads()->boost(
Expand Down Expand Up @@ -546,12 +547,33 @@ $client->ads()->deleteCampaign($copyId, $workspaceId, $connectionId);
// deleteNetworkAd(), duplicateAdSet(), duplicateNetworkAd(), creatives(), creative(), deleteCreative()
```

### Forecasts, conversions and the ad library

Available wherever the network's `capabilities` say so — `providers()` reports them.

```php
$client->ads()->bidPricing($workspaceId, $connectionId, $adAccountId, 'traffic', $targeting);
$client->ads()->supplyForecast($workspaceId, $connectionId, $adAccountId, 'traffic', $targeting, budgetMinor: 50000);

$rules = $client->ads()->conversionRules($workspaceId, $connectionId, $adAccountId);
$ruleId = $client->ads()->createConversionRule(
$workspaceId, $connectionId, $adAccountId, 'Checkout', 'purchase', 'last_touch',
);
$client->ads()->attachConversionRule($ruleId, $workspaceId, $connectionId, $adSetId);
$client->ads()->conversionMetrics($ruleId, $workspaceId, $connectionId, '2026-09-01', '2026-09-30');
// Each event needs happenedAt in epoch ms and an email or a clickId; the API hashes the address.
$client->ads()->sendConversionEvents($ruleId, $workspaceId, $connectionId, $events);

$page = $client->ads()->adLibrary($workspaceId, $connectionId, keyword: 'crm', countries: ['US']);
```

### Audiences, reach and insights

```php
$client->ads()->audience($audienceId, $connectionId);
$client->ads()->updateAudience($audienceId, $workspaceId, $connectionId, name: 'Customers 2026');
$added = $client->ads()->addAudienceUsers($audienceId, $workspaceId, $connectionId, $emails); // hashed by the API
$client->ads()->addAudienceCompanies($audienceId, $workspaceId, $connectionId, $companies); // a company list
$client->ads()->deleteAudience($audienceId, $workspaceId, $connectionId);

$reach = $client->ads()->estimateReach($workspaceId, $connectionId, 'act_123', '555', [
Expand Down
46 changes: 46 additions & 0 deletions src/Model/AdProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** An ad network from the API's registry. `configured` false cannot be connected yet. */
final class AdProvider extends Model
{
/**
* @param array<string, bool> $capabilities campaigns, audiences, conversions, forecasts, ...
* @param array<int, string> $targetingFacets what searchTargeting() accepts here
* @param array<int, array<string, mixed>> $trackingMacros macros expanded in a creative's tracking parameters
*/
private function __construct(
array $raw,
public readonly string $id,
public readonly string $name,
public readonly ?string $logo,
public readonly bool $configured,
/** @var array<int, string> */
public readonly array $connectMethods,
public readonly array $capabilities,
public readonly array $targetingFacets,
public readonly array $trackingMacros,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::requiredStr($data, 'id'),
self::requiredStr($data, 'name'),
self::str($data, 'logo'),
self::bool($data, 'configured') ?? false,
array_values(array_filter(self::seq($data, 'connect_methods'), 'is_string')),
array_filter(self::map($data, 'capabilities'), 'is_bool'),
array_values(array_filter(self::seq($data, 'targeting_facets'), 'is_string')),
array_values(array_filter(self::seq($data, 'tracking_macros'), 'is_array')),
);
}
}
34 changes: 34 additions & 0 deletions src/Model/BidPricing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** What the auction costs, in minor units of the ad account currency. */
final class BidPricing extends Model
{
private function __construct(
array $raw,
public readonly ?string $currency,
public readonly ?int $suggestedBidMinor,
public readonly ?int $minBidMinor,
public readonly ?int $maxBidMinor,
public readonly ?int $dailyBudgetFloorMinor,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::str($data, 'currency'),
self::int($data, 'suggested_bid_minor'),
self::int($data, 'min_bid_minor'),
self::int($data, 'max_bid_minor'),
self::int($data, 'daily_budget_floor_minor'),
);
}
}
34 changes: 34 additions & 0 deletions src/Model/ConversionMetrics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** What a conversion rule recorded over a date range. */
final class ConversionMetrics extends Model
{
private function __construct(
array $raw,
public readonly int $conversions,
public readonly int $postClickConversions,
public readonly int $viewThroughConversions,
public readonly int $valueMinor,
public readonly ?int $costPerConversionMinor,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::int($data, 'conversions') ?? 0,
self::int($data, 'post_click_conversions') ?? 0,
self::int($data, 'view_through_conversions') ?? 0,
self::int($data, 'value_minor') ?? 0,
self::int($data, 'cost_per_conversion_minor'),
);
}
}
47 changes: 47 additions & 0 deletions src/Model/ConversionRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** How the network attributes a sale or a sign-up back to an ad set. */
final class ConversionRule extends Model
{
/** @param array<int, string> $campaignIds ad sets this rule is attached to */
private function __construct(
array $raw,
public readonly string $id,
public readonly string $name,
public readonly ?string $type,
public readonly ?string $attribution,
public readonly int $postClickWindowDays,
public readonly int $viewThroughWindowDays,
public readonly ?int $valueMinor,
public readonly ?string $currency,
public readonly bool $enabled,
public readonly ?string $createdAt,
public readonly array $campaignIds,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::requiredStr($data, 'id'),
self::requiredStr($data, 'name'),
self::str($data, 'type'),
self::str($data, 'attribution'),
self::int($data, 'post_click_window_days') ?? 30,
self::int($data, 'view_through_window_days') ?? 7,
self::int($data, 'value_minor'),
self::str($data, 'currency'),
self::bool($data, 'enabled') ?? true,
self::str($data, 'created_at'),
array_values(array_filter(self::seq($data, 'campaign_ids'), 'is_string')),
);
}
}
36 changes: 36 additions & 0 deletions src/Model/SupplyForecast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** What an audience would deliver at a budget, over the network's own window. */
final class SupplyForecast extends Model
{
private function __construct(
array $raw,
public readonly ?string $currency,
public readonly ?int $impressions,
public readonly ?int $clicks,
public readonly ?int $spendMinor,
public readonly ?int $windowDays,
public readonly bool $ready,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::str($data, 'currency'),
self::int($data, 'impressions'),
self::int($data, 'clicks'),
self::int($data, 'spend_minor'),
self::int($data, 'window_days'),
self::bool($data, 'ready') ?? false,
);
}
}
Loading
Loading