From dc4d10e5f96a59c72619e74e64f1e84af93ca886 Mon Sep 17 00:00:00 2001 From: Ali Hesari Date: Sun, 20 Sep 2026 03:55:45 +0200 Subject: [PATCH] feat(ads): add catalogs, reach and frequency, the ad archive and account settings Also adds goals() so a caller asks the connection what it can run rather than assuming. --- README.md | 2 +- .../java/com/fopost/sdk/model/AdActivity.java | 11 + .../java/com/fopost/sdk/model/AdLabel.java | 7 + .../com/fopost/sdk/model/AdLibraryEntry.java | 19 + .../com/fopost/sdk/model/AdLibraryPage.java | 6 + .../java/com/fopost/sdk/model/AdStudy.java | 11 + .../fopost/sdk/model/CatalogBatchResult.java | 6 + .../com/fopost/sdk/model/CatalogProduct.java | 14 + .../fopost/sdk/model/CatalogProductsPage.java | 6 + .../fopost/sdk/model/HighDemandPeriod.java | 9 + .../fopost/sdk/model/IosCampaignLimits.java | 7 + .../fopost/sdk/model/PartnershipCreator.java | 9 + .../com/fopost/sdk/model/ProductCatalog.java | 8 + .../com/fopost/sdk/model/ProductFeed.java | 9 + .../fopost/sdk/model/ProductFeedUpload.java | 10 + .../java/com/fopost/sdk/model/ProductSet.java | 8 + .../sdk/model/ReachFrequencyPrediction.java | 14 + .../java/com/fopost/sdk/model/ValueRule.java | 6 + .../com/fopost/sdk/model/ValueRuleSet.java | 8 + .../com/fopost/sdk/param/AdLabelParams.java | 29 + .../fopost/sdk/param/ApplyAdLabelParams.java | 34 ++ .../sdk/param/CatalogProductBatchParams.java | 54 ++ .../sdk/param/CatalogProductParams.java | 54 ++ .../fopost/sdk/param/CreateAdStudyParams.java | 55 ++ .../fopost/sdk/param/CreateCatalogParams.java | 34 ++ .../param/CreateHighDemandPeriodParams.java | 38 ++ .../sdk/param/CreateProductFeedParams.java | 41 ++ .../sdk/param/CreateReachFrequencyParams.java | 48 ++ .../sdk/param/CreateValueRuleSetParams.java | 39 ++ .../fopost/sdk/param/PartnershipParams.java | 29 + .../fopost/sdk/param/ProductSetParams.java | 35 ++ .../sdk/param/ReachFrequencyActionParams.java | 29 + .../sdk/param/StartFeedUploadParams.java | 30 ++ .../fopost/sdk/param/UpdateCatalogParams.java | 27 + .../com/fopost/sdk/resource/AdsResource.java | 501 +++++++++++++++++- 35 files changed, 1245 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/fopost/sdk/model/AdActivity.java create mode 100644 src/main/java/com/fopost/sdk/model/AdLabel.java create mode 100644 src/main/java/com/fopost/sdk/model/AdLibraryEntry.java create mode 100644 src/main/java/com/fopost/sdk/model/AdLibraryPage.java create mode 100644 src/main/java/com/fopost/sdk/model/AdStudy.java create mode 100644 src/main/java/com/fopost/sdk/model/CatalogBatchResult.java create mode 100644 src/main/java/com/fopost/sdk/model/CatalogProduct.java create mode 100644 src/main/java/com/fopost/sdk/model/CatalogProductsPage.java create mode 100644 src/main/java/com/fopost/sdk/model/HighDemandPeriod.java create mode 100644 src/main/java/com/fopost/sdk/model/IosCampaignLimits.java create mode 100644 src/main/java/com/fopost/sdk/model/PartnershipCreator.java create mode 100644 src/main/java/com/fopost/sdk/model/ProductCatalog.java create mode 100644 src/main/java/com/fopost/sdk/model/ProductFeed.java create mode 100644 src/main/java/com/fopost/sdk/model/ProductFeedUpload.java create mode 100644 src/main/java/com/fopost/sdk/model/ProductSet.java create mode 100644 src/main/java/com/fopost/sdk/model/ReachFrequencyPrediction.java create mode 100644 src/main/java/com/fopost/sdk/model/ValueRule.java create mode 100644 src/main/java/com/fopost/sdk/model/ValueRuleSet.java create mode 100644 src/main/java/com/fopost/sdk/param/AdLabelParams.java create mode 100644 src/main/java/com/fopost/sdk/param/ApplyAdLabelParams.java create mode 100644 src/main/java/com/fopost/sdk/param/CatalogProductBatchParams.java create mode 100644 src/main/java/com/fopost/sdk/param/CatalogProductParams.java create mode 100644 src/main/java/com/fopost/sdk/param/CreateAdStudyParams.java create mode 100644 src/main/java/com/fopost/sdk/param/CreateCatalogParams.java create mode 100644 src/main/java/com/fopost/sdk/param/CreateHighDemandPeriodParams.java create mode 100644 src/main/java/com/fopost/sdk/param/CreateProductFeedParams.java create mode 100644 src/main/java/com/fopost/sdk/param/CreateReachFrequencyParams.java create mode 100644 src/main/java/com/fopost/sdk/param/CreateValueRuleSetParams.java create mode 100644 src/main/java/com/fopost/sdk/param/PartnershipParams.java create mode 100644 src/main/java/com/fopost/sdk/param/ProductSetParams.java create mode 100644 src/main/java/com/fopost/sdk/param/ReachFrequencyActionParams.java create mode 100644 src/main/java/com/fopost/sdk/param/StartFeedUploadParams.java create mode 100644 src/main/java/com/fopost/sdk/param/UpdateCatalogParams.java diff --git a/README.md b/README.md index 343931c..4c6aba9 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ long failed = client.posts().stream(PostListParams.create().workspaceId(workspac | `media()` | `list`, `upload`, `presign`, `complete`, `uploadDirect`, `delete` | | `ai()` | `credits`, `generateCaption`, `rewrite`, `repurposeUrl` | | `inbox()` | `list`, `threads`, `conversations`, `unreadCount`, `accounts`, `platforms`, `markThreadRead`, `markConversationRead`, `refresh`, `update`, `editComment`, `reply`, `hide`, `unhide`, `delete`, `like`, `unlike`, `pin`, `unpin`, `react`, `startConversation`, `setTyping`, `listApprovals`, `approveReply`, `rejectReply` | -| `ads()` | `list`, `external`, `boostable`, `connections`, `sources`, `authorizeMeta`, `deleteConnection`, `boost`, `create`, `refresh`, `setStatus`, `delete`, `accountTree`, `createCampaign`, `campaign`, `updateCampaign`, `deleteCampaign`, `duplicateCampaign`, `createAdSet`, `adSet`, `updateAdSet`, `deleteAdSet`, `duplicateAdSet`, `createNetworkAd`, `networkAd`, `updateNetworkAd`, `deleteNetworkAd`, `duplicateNetworkAd`, `bulkSetStatus`, `creatives`, `createCreative`, `creative`, `deleteCreative`, `estimateReach`, `insights`, `adInsights`, `audiences`, `createAudience`, `audience`, `updateAudience`, `deleteAudience`, `addAudienceUsers`, `searchTargeting`, `leadForms`, `createLeadForm`, `leadForm`, `archiveLeadForm`, `leads`, `leadsFeed`, `leadPages`, `subscribeLeadPage`, `unsubscribeLeadPage` | +| `ads()` | `list`, `external`, `boostable`, `connections`, `sources`, `authorizeMeta`, `deleteConnection`, `boost`, `create`, `refresh`, `setStatus`, `delete`, `accountTree`, `createCampaign`, `campaign`, `updateCampaign`, `deleteCampaign`, `duplicateCampaign`, `createAdSet`, `adSet`, `updateAdSet`, `deleteAdSet`, `duplicateAdSet`, `createNetworkAd`, `networkAd`, `updateNetworkAd`, `deleteNetworkAd`, `duplicateNetworkAd`, `bulkSetStatus`, `creatives`, `createCreative`, `creative`, `deleteCreative`, `estimateReach`, `insights`, `adInsights`, `audiences`, `createAudience`, `audience`, `updateAudience`, `deleteAudience`, `addAudienceUsers`, `searchTargeting`, `leadForms`, `createLeadForm`, `leadForm`, `archiveLeadForm`, `leads`, `leadsFeed`, `leadPages`, `subscribeLeadPage`, `unsubscribeLeadPage`, `goals`, `catalogs`, `createCatalog`, `catalog`, `updateCatalog`, `deleteCatalog`, `catalogProducts`, `writeCatalogProducts`, `productFeeds`, `createProductFeed`, `deleteProductFeed`, `feedUploads`, `startFeedUpload`, `productSets`, `createProductSet`, `updateProductSet`, `deleteProductSet`, `reachFrequency`, `createReachFrequency`, `reachFrequencyPrediction`, `reserveReachFrequency`, `cancelReachFrequency`, `library`, `partnershipCreators`, `requestPartnership`, `revokePartnership`, `accountActivity`, `labels`, `createLabel`, `updateLabel`, `deleteLabel`, `applyLabel`, `studies`, `createStudy`, `study`, `deleteStudy`, `iosCampaignLimits`, `highDemandPeriods`, `createHighDemandPeriod`, `deleteHighDemandPeriod`, `valueRuleSets`, `createValueRuleSet`, `deleteValueRuleSet` | | `validate()` | `post`, `length`, `media` | `accounts().communities()` covers the X communities an account can post into: `list`, `sync`, diff --git a/src/main/java/com/fopost/sdk/model/AdActivity.java b/src/main/java/com/fopost/sdk/model/AdActivity.java new file mode 100644 index 0000000..71e6efb --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/AdActivity.java @@ -0,0 +1,11 @@ +package com.fopost.sdk.model; + +/** One change recorded on an ad account. */ +public record AdActivity( + String id, + String eventType, + String actorName, + String objectName, + String objectType, + String extraData, + String createdAt) {} diff --git a/src/main/java/com/fopost/sdk/model/AdLabel.java b/src/main/java/com/fopost/sdk/model/AdLabel.java new file mode 100644 index 0000000..88069f6 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/AdLabel.java @@ -0,0 +1,7 @@ +package com.fopost.sdk.model; + +/** Groups campaigns, ad sets and ads for reporting. */ +public record AdLabel( + String id, + String name, + String createdAt) {} diff --git a/src/main/java/com/fopost/sdk/model/AdLibraryEntry.java b/src/main/java/com/fopost/sdk/model/AdLibraryEntry.java new file mode 100644 index 0000000..5cda3c5 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/AdLibraryEntry.java @@ -0,0 +1,19 @@ +package com.fopost.sdk.model; + +/** One public archive entry. Read live on every search and stored nowhere. */ +public record AdLibraryEntry( + String id, + String pageId, + String pageName, + java.util.List bodies, + java.util.List titles, + java.util.List linkUrls, + String snapshotUrl, + java.util.List publisherPlatforms, + String startedAt, + String endedAt, + String currency, + Long spendLower, + Long spendUpper, + Long impressionsLower, + Long impressionsUpper) {} diff --git a/src/main/java/com/fopost/sdk/model/AdLibraryPage.java b/src/main/java/com/fopost/sdk/model/AdLibraryPage.java new file mode 100644 index 0000000..16a0cf8 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/AdLibraryPage.java @@ -0,0 +1,6 @@ +package com.fopost.sdk.model; + +/** One page of archive results; pass {@code nextCursor} back as {@code after}. */ +public record AdLibraryPage( + java.util.List entries, + String nextCursor) {} diff --git a/src/main/java/com/fopost/sdk/model/AdStudy.java b/src/main/java/com/fopost/sdk/model/AdStudy.java new file mode 100644 index 0000000..7a4d551 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/AdStudy.java @@ -0,0 +1,11 @@ +package com.fopost.sdk.model; + +/** An A/B study splitting traffic across its cells. */ +public record AdStudy( + String id, + String name, + String description, + String type, + String status, + String startAt, + String endAt) {} diff --git a/src/main/java/com/fopost/sdk/model/CatalogBatchResult.java b/src/main/java/com/fopost/sdk/model/CatalogBatchResult.java new file mode 100644 index 0000000..beb9f75 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/CatalogBatchResult.java @@ -0,0 +1,6 @@ +package com.fopost.sdk.model; + +/** What a catalog product batch was accepted as. */ +public record CatalogBatchResult( + java.util.List handles, + long accepted) {} diff --git a/src/main/java/com/fopost/sdk/model/CatalogProduct.java b/src/main/java/com/fopost/sdk/model/CatalogProduct.java new file mode 100644 index 0000000..0e7db03 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/CatalogProduct.java @@ -0,0 +1,14 @@ +package com.fopost.sdk.model; + +/** One product in a catalog. {@code priceMinor} is minor units of {@code currency}. */ +public record CatalogProduct( + String id, + String retailerId, + String name, + String description, + String availability, + String condition, + Long priceMinor, + String currency, + String imageUrl, + String url) {} diff --git a/src/main/java/com/fopost/sdk/model/CatalogProductsPage.java b/src/main/java/com/fopost/sdk/model/CatalogProductsPage.java new file mode 100644 index 0000000..ed8ca4b --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/CatalogProductsPage.java @@ -0,0 +1,6 @@ +package com.fopost.sdk.model; + +/** One page of catalog products; pass {@code nextCursor} back as {@code after}. */ +public record CatalogProductsPage( + java.util.List products, + String nextCursor) {} diff --git a/src/main/java/com/fopost/sdk/model/HighDemandPeriod.java b/src/main/java/com/fopost/sdk/model/HighDemandPeriod.java new file mode 100644 index 0000000..098cc8c --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/HighDemandPeriod.java @@ -0,0 +1,9 @@ +package com.fopost.sdk.model; + +/** A window the network should expect heavier spend over. */ +public record HighDemandPeriod( + String id, + String startAt, + String endAt, + Double budgetValue, + String budgetValueType) {} diff --git a/src/main/java/com/fopost/sdk/model/IosCampaignLimits.java b/src/main/java/com/fopost/sdk/model/IosCampaignLimits.java new file mode 100644 index 0000000..0ef7574 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/IosCampaignLimits.java @@ -0,0 +1,7 @@ +package com.fopost.sdk.model; + +/** How many iOS 14 campaigns an ad account may run at once, per app. */ +public record IosCampaignLimits( + Long limit, + Long used, + String appId) {} diff --git a/src/main/java/com/fopost/sdk/model/PartnershipCreator.java b/src/main/java/com/fopost/sdk/model/PartnershipCreator.java new file mode 100644 index 0000000..5b65184 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/PartnershipCreator.java @@ -0,0 +1,9 @@ +package com.fopost.sdk.model; + +/** A creator who allowlisted this advertiser for partnership ads. */ +public record PartnershipCreator( + String id, + String username, + String name, + String status, + java.util.List permissions) {} diff --git a/src/main/java/com/fopost/sdk/model/ProductCatalog.java b/src/main/java/com/fopost/sdk/model/ProductCatalog.java new file mode 100644 index 0000000..4a7d71b --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/ProductCatalog.java @@ -0,0 +1,8 @@ +package com.fopost.sdk.model; + +/** A product catalog on the connection's business portfolio, read live. */ +public record ProductCatalog( + String id, + String name, + String vertical, + Long productCount) {} diff --git a/src/main/java/com/fopost/sdk/model/ProductFeed.java b/src/main/java/com/fopost/sdk/model/ProductFeed.java new file mode 100644 index 0000000..e02e4be --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/ProductFeed.java @@ -0,0 +1,9 @@ +package com.fopost.sdk.model; + +/** Keeps a catalog in step with a product file you host. */ +public record ProductFeed( + String id, + String name, + String url, + String schedule, + String createdAt) {} diff --git a/src/main/java/com/fopost/sdk/model/ProductFeedUpload.java b/src/main/java/com/fopost/sdk/model/ProductFeedUpload.java new file mode 100644 index 0000000..c147b84 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/ProductFeedUpload.java @@ -0,0 +1,10 @@ +package com.fopost.sdk.model; + +/** One run the network made of a product feed. */ +public record ProductFeedUpload( + String id, + String startedAt, + String endedAt, + String status, + Long errorCount, + Long warningCount) {} diff --git a/src/main/java/com/fopost/sdk/model/ProductSet.java b/src/main/java/com/fopost/sdk/model/ProductSet.java new file mode 100644 index 0000000..febfde3 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/ProductSet.java @@ -0,0 +1,8 @@ +package com.fopost.sdk.model; + +/** The slice of a catalog one catalog ad runs from. */ +public record ProductSet( + String id, + String name, + Long productCount, + java.util.Map filter) {} diff --git a/src/main/java/com/fopost/sdk/model/ReachFrequencyPrediction.java b/src/main/java/com/fopost/sdk/model/ReachFrequencyPrediction.java new file mode 100644 index 0000000..82c6b20 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/ReachFrequencyPrediction.java @@ -0,0 +1,14 @@ +package com.fopost.sdk.model; + +/** A priced flight. Nothing is bought until it is reserved. */ +public record ReachFrequencyPrediction( + String id, + String name, + String status, + Long reach, + Long impressions, + Long frequencyCap, + Long budgetMinor, + String startAt, + String endAt, + boolean reserved) {} diff --git a/src/main/java/com/fopost/sdk/model/ValueRule.java b/src/main/java/com/fopost/sdk/model/ValueRule.java new file mode 100644 index 0000000..58a8871 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/ValueRule.java @@ -0,0 +1,6 @@ +package com.fopost.sdk.model; + +/** Weights one condition's conversions. */ +public record ValueRule( + String condition, + Double multiplier) {} diff --git a/src/main/java/com/fopost/sdk/model/ValueRuleSet.java b/src/main/java/com/fopost/sdk/model/ValueRuleSet.java new file mode 100644 index 0000000..ff0b29e --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/ValueRuleSet.java @@ -0,0 +1,8 @@ +package com.fopost.sdk.model; + +/** Weights conversions so some audiences count for more than others. */ +public record ValueRuleSet( + String id, + String name, + String status, + java.util.List rules) {} diff --git a/src/main/java/com/fopost/sdk/param/AdLabelParams.java b/src/main/java/com/fopost/sdk/param/AdLabelParams.java new file mode 100644 index 0000000..e7b1ccb --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/AdLabelParams.java @@ -0,0 +1,29 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** Creates or renames an ad label. */ +public final class AdLabelParams { + + private final Map body = new LinkedHashMap<>(); + + private AdLabelParams() {} + + public static AdLabelParams of( + String workspaceId, + String connectionId, + String adAccountId, + String name) { + AdLabelParams params = new AdLabelParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("adAccountId", adAccountId); + params.body.put("name", name); + return params; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/ApplyAdLabelParams.java b/src/main/java/com/fopost/sdk/param/ApplyAdLabelParams.java new file mode 100644 index 0000000..073c250 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/ApplyAdLabelParams.java @@ -0,0 +1,34 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Puts a label on a campaign, ad set or ad, keeping whatever labels it already carries. + * {@code level} is campaign, ad_set or ad. + */ +public final class ApplyAdLabelParams { + + private final Map body = new LinkedHashMap<>(); + + private ApplyAdLabelParams() {} + + public static ApplyAdLabelParams of( + String workspaceId, + String connectionId, + String adAccountId, + String objectId, + String level) { + ApplyAdLabelParams params = new ApplyAdLabelParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("adAccountId", adAccountId); + params.body.put("objectId", objectId); + params.body.put("level", level); + return params; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/CatalogProductBatchParams.java b/src/main/java/com/fopost/sdk/param/CatalogProductBatchParams.java new file mode 100644 index 0000000..0b05056 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/CatalogProductBatchParams.java @@ -0,0 +1,54 @@ +package com.fopost.sdk.param; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Up to 500 product upserts and deletes in one batch, keyed by your own retailer id. Upserts and + * deletes travel together. Also needs the {@code publish} scope. + * + *
{@code
+ * CatalogProductBatchParams.of(workspaceId, connectionId)
+ *         .upsert(CatalogProductParams.of(
+ *                 "SKU-1042", "Trail Runner",
+ *                 "https://yourbrand.com/shop/trail-runner",
+ *                 "https://yourbrand.com/img/trail-runner.jpg",
+ *                 12900, "USD"))
+ *         .delete("SKU-0007");
+ * }
+ */ +public final class CatalogProductBatchParams { + + private final Map body = new LinkedHashMap<>(); + private final List> products = new ArrayList<>(); + + private CatalogProductBatchParams() {} + + public static CatalogProductBatchParams of(String workspaceId, String connectionId) { + CatalogProductBatchParams params = new CatalogProductBatchParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + return params; + } + + public CatalogProductBatchParams upsert(CatalogProductParams product) { + products.add(product.toMap()); + return this; + } + + public CatalogProductBatchParams delete(String retailerId) { + Map row = new LinkedHashMap<>(); + row.put("op", "delete"); + row.put("retailerId", retailerId); + products.add(row); + return this; + } + + public Map toMap() { + Map out = new LinkedHashMap<>(body); + out.put("products", new ArrayList<>(products)); + return out; + } +} diff --git a/src/main/java/com/fopost/sdk/param/CatalogProductParams.java b/src/main/java/com/fopost/sdk/param/CatalogProductParams.java new file mode 100644 index 0000000..31fff02 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/CatalogProductParams.java @@ -0,0 +1,54 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * One product in a catalog batch. {@code priceMinor} is minor units of {@code currency}, so 12900 + * with USD is $129.00. + */ +public final class CatalogProductParams { + + private final Map body = new LinkedHashMap<>(); + + private CatalogProductParams() {} + + public static CatalogProductParams of( + String retailerId, String name, String url, String imageUrl, long priceMinor, String currency) { + CatalogProductParams params = new CatalogProductParams(); + params.body.put("op", "upsert"); + params.body.put("retailerId", retailerId); + params.body.put("name", name); + params.body.put("url", url); + params.body.put("imageUrl", imageUrl); + params.body.put("priceMinor", priceMinor); + params.body.put("currency", currency); + return params; + } + + public CatalogProductParams description(String description) { + body.put("description", description); + return this; + } + + /** in stock, out of stock, preorder, available for order or discontinued. */ + public CatalogProductParams availability(String availability) { + body.put("availability", availability); + return this; + } + + /** new, refurbished or used. */ + public CatalogProductParams condition(String condition) { + body.put("condition", condition); + return this; + } + + public CatalogProductParams brand(String brand) { + body.put("brand", brand); + return this; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/CreateAdStudyParams.java b/src/main/java/com/fopost/sdk/param/CreateAdStudyParams.java new file mode 100644 index 0000000..67d4005 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/CreateAdStudyParams.java @@ -0,0 +1,55 @@ +package com.fopost.sdk.param; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * An A/B study splitting traffic evenly across two to five cells for the length of the flight. + * Times are ISO 8601. + */ +public final class CreateAdStudyParams { + + private final Map body = new LinkedHashMap<>(); + private final List> cells = new ArrayList<>(); + + private CreateAdStudyParams() {} + + public static CreateAdStudyParams of( + String workspaceId, + String connectionId, + String adAccountId, + String name, + String startAt, + String endAt) { + CreateAdStudyParams params = new CreateAdStudyParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("adAccountId", adAccountId); + params.body.put("name", name); + params.body.put("startAt", startAt); + params.body.put("endAt", endAt); + return params; + } + + /** One arm of the study: the campaigns it tests. */ + public CreateAdStudyParams cell(String name, List objectIds) { + Map cell = new LinkedHashMap<>(); + cell.put("name", name); + cell.put("objectIds", objectIds); + cells.add(cell); + return this; + } + + public CreateAdStudyParams description(String description) { + body.put("description", description); + return this; + } + + public Map toMap() { + Map out = new LinkedHashMap<>(body); + out.put("cells", new ArrayList<>(cells)); + return out; + } +} diff --git a/src/main/java/com/fopost/sdk/param/CreateCatalogParams.java b/src/main/java/com/fopost/sdk/param/CreateCatalogParams.java new file mode 100644 index 0000000..fa80d1b --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/CreateCatalogParams.java @@ -0,0 +1,34 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A product catalog on the connection's business portfolio. Also needs the {@code publish} scope. + */ +public final class CreateCatalogParams { + + private final Map body = new LinkedHashMap<>(); + + private CreateCatalogParams() {} + + public static CreateCatalogParams of( + String workspaceId, + String connectionId, + String name) { + CreateCatalogParams params = new CreateCatalogParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("name", name); + return params; + } + + /** The network's catalog vertical; commerce when unset. */ + public CreateCatalogParams vertical(String vertical) { + body.put("vertical", vertical); + return this; + } + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/CreateHighDemandPeriodParams.java b/src/main/java/com/fopost/sdk/param/CreateHighDemandPeriodParams.java new file mode 100644 index 0000000..6b80eeb --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/CreateHighDemandPeriodParams.java @@ -0,0 +1,38 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Tells the network to expect heavier spend over a window, so pacing allows for it. + * {@code budgetValueType} is ABSOLUTE or MULTIPLIER. Times are ISO 8601. + */ +public final class CreateHighDemandPeriodParams { + + private final Map body = new LinkedHashMap<>(); + + private CreateHighDemandPeriodParams() {} + + public static CreateHighDemandPeriodParams of( + String workspaceId, + String connectionId, + String adAccountId, + String startAt, + String endAt, + double budgetValue, + String budgetValueType) { + CreateHighDemandPeriodParams params = new CreateHighDemandPeriodParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("adAccountId", adAccountId); + params.body.put("startAt", startAt); + params.body.put("endAt", endAt); + params.body.put("budgetValue", budgetValue); + params.body.put("budgetValueType", budgetValueType); + return params; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/CreateProductFeedParams.java b/src/main/java/com/fopost/sdk/param/CreateProductFeedParams.java new file mode 100644 index 0000000..c84e5f7 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/CreateProductFeedParams.java @@ -0,0 +1,41 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A product feed. A {@link #schedule(String)} needs a {@link #url(String)}. Also needs the + * {@code publish} scope. + */ +public final class CreateProductFeedParams { + + private final Map body = new LinkedHashMap<>(); + + private CreateProductFeedParams() {} + + public static CreateProductFeedParams of( + String workspaceId, + String connectionId, + String name) { + CreateProductFeedParams params = new CreateProductFeedParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("name", name); + return params; + } + + /** Where the network fetches the file; omit for manual uploads. */ + public CreateProductFeedParams url(String url) { + body.put("url", url); + return this; + } + + /** HOURLY, DAILY or WEEKLY. Needs a url. */ + public CreateProductFeedParams schedule(String schedule) { + body.put("schedule", schedule); + return this; + } + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/CreateReachFrequencyParams.java b/src/main/java/com/fopost/sdk/param/CreateReachFrequencyParams.java new file mode 100644 index 0000000..6570080 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/CreateReachFrequencyParams.java @@ -0,0 +1,48 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Prices a flight before you buy it. Nothing is reserved until you reserve it. Times are ISO 8601. + */ +public final class CreateReachFrequencyParams { + + private final Map body = new LinkedHashMap<>(); + + private CreateReachFrequencyParams() {} + + public static CreateReachFrequencyParams of( + String workspaceId, + String connectionId, + String adAccountId, + String name, + AdTargetingParams targeting, + List placements, + long budgetMinor, + String startAt, + String endAt) { + CreateReachFrequencyParams params = new CreateReachFrequencyParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("adAccountId", adAccountId); + params.body.put("name", name); + params.body.put("targeting", targeting.toMap()); + params.body.put("placements", placements); + params.body.put("budgetMinor", budgetMinor); + params.body.put("startAt", startAt); + params.body.put("endAt", endAt); + return params; + } + + /** How often one person should see the ad over the flight. */ + public CreateReachFrequencyParams frequencyCap(int frequencyCap) { + body.put("frequencyCap", frequencyCap); + return this; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/CreateValueRuleSetParams.java b/src/main/java/com/fopost/sdk/param/CreateValueRuleSetParams.java new file mode 100644 index 0000000..edd0105 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/CreateValueRuleSetParams.java @@ -0,0 +1,39 @@ +package com.fopost.sdk.param; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Weights conversions so some audiences count for more than others: one to twenty rules. */ +public final class CreateValueRuleSetParams { + + private final Map body = new LinkedHashMap<>(); + private final List> rules = new ArrayList<>(); + + private CreateValueRuleSetParams() {} + + public static CreateValueRuleSetParams of( + String workspaceId, String connectionId, String adAccountId, String name) { + CreateValueRuleSetParams params = new CreateValueRuleSetParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("adAccountId", adAccountId); + params.body.put("name", name); + return params; + } + + public CreateValueRuleSetParams rule(String condition, double multiplier) { + Map row = new LinkedHashMap<>(); + row.put("condition", condition); + row.put("multiplier", multiplier); + rules.add(row); + return this; + } + + public Map toMap() { + Map out = new LinkedHashMap<>(body); + out.put("rules", new ArrayList<>(rules)); + return out; + } +} diff --git a/src/main/java/com/fopost/sdk/param/PartnershipParams.java b/src/main/java/com/fopost/sdk/param/PartnershipParams.java new file mode 100644 index 0000000..c9c3ebd --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/PartnershipParams.java @@ -0,0 +1,29 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** Asks a creator for partnership permission. */ +public final class PartnershipParams { + + private final Map body = new LinkedHashMap<>(); + + private PartnershipParams() {} + + public static PartnershipParams of( + String workspaceId, + String connectionId, + String pageId, + String creatorId) { + PartnershipParams params = new PartnershipParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("pageId", pageId); + params.body.put("creatorId", creatorId); + return params; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/ProductSetParams.java b/src/main/java/com/fopost/sdk/param/ProductSetParams.java new file mode 100644 index 0000000..fc819c7 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/ProductSetParams.java @@ -0,0 +1,35 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A product set: the slice of a catalog one catalog ad runs from. Without a + * {@link #filter(java.util.Map)} the set is the whole catalog. Also needs the {@code publish} scope. + */ +public final class ProductSetParams { + + private final Map body = new LinkedHashMap<>(); + + private ProductSetParams() {} + + public static ProductSetParams of( + String workspaceId, + String connectionId, + String name) { + ProductSetParams params = new ProductSetParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("name", name); + return params; + } + + /** The network's own product-set filter. */ + public ProductSetParams filter(java.util.Map filter) { + body.put("filter", filter); + return this; + } + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/ReachFrequencyActionParams.java b/src/main/java/com/fopost/sdk/param/ReachFrequencyActionParams.java new file mode 100644 index 0000000..0f2a38f --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/ReachFrequencyActionParams.java @@ -0,0 +1,29 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Reserves or cancels a reach-and-frequency prediction. Also needs the {@code publish} scope. + */ +public final class ReachFrequencyActionParams { + + private final Map body = new LinkedHashMap<>(); + + private ReachFrequencyActionParams() {} + + public static ReachFrequencyActionParams of( + String workspaceId, + String connectionId, + String adAccountId) { + ReachFrequencyActionParams params = new ReachFrequencyActionParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("adAccountId", adAccountId); + return params; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/StartFeedUploadParams.java b/src/main/java/com/fopost/sdk/param/StartFeedUploadParams.java new file mode 100644 index 0000000..c6939f9 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/StartFeedUploadParams.java @@ -0,0 +1,30 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** Fetches a feed now. Also needs the {@code publish} scope. */ +public final class StartFeedUploadParams { + + private final Map body = new LinkedHashMap<>(); + + private StartFeedUploadParams() {} + + public static StartFeedUploadParams of( + String workspaceId, + String connectionId) { + StartFeedUploadParams params = new StartFeedUploadParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + return params; + } + + /** Overrides the feed's own url for this run. */ + public StartFeedUploadParams url(String url) { + body.put("url", url); + return this; + } + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/UpdateCatalogParams.java b/src/main/java/com/fopost/sdk/param/UpdateCatalogParams.java new file mode 100644 index 0000000..75334f3 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/UpdateCatalogParams.java @@ -0,0 +1,27 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** Renames a catalog. Also needs the {@code publish} scope. */ +public final class UpdateCatalogParams { + + private final Map body = new LinkedHashMap<>(); + + private UpdateCatalogParams() {} + + public static UpdateCatalogParams of( + String workspaceId, + String connectionId, + String name) { + UpdateCatalogParams params = new UpdateCatalogParams(); + params.body.put("workspaceId", workspaceId); + params.body.put("connectionId", connectionId); + params.body.put("name", name); + return params; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/resource/AdsResource.java b/src/main/java/com/fopost/sdk/resource/AdsResource.java index 96d54c4..857a26d 100644 --- a/src/main/java/com/fopost/sdk/resource/AdsResource.java +++ b/src/main/java/com/fopost/sdk/resource/AdsResource.java @@ -2,6 +2,35 @@ import com.fopost.sdk.internal.ApiClient; import com.fopost.sdk.model.Ad; +import com.fopost.sdk.model.AdActivity; +import com.fopost.sdk.model.AdLabel; +import com.fopost.sdk.model.AdLibraryPage; +import com.fopost.sdk.model.AdStudy; +import com.fopost.sdk.model.CatalogBatchResult; +import com.fopost.sdk.model.CatalogProductsPage; +import com.fopost.sdk.model.HighDemandPeriod; +import com.fopost.sdk.model.IosCampaignLimits; +import com.fopost.sdk.model.PartnershipCreator; +import com.fopost.sdk.model.ProductCatalog; +import com.fopost.sdk.model.ProductFeed; +import com.fopost.sdk.model.ProductFeedUpload; +import com.fopost.sdk.model.ProductSet; +import com.fopost.sdk.model.ReachFrequencyPrediction; +import com.fopost.sdk.model.ValueRuleSet; +import com.fopost.sdk.param.AdLabelParams; +import com.fopost.sdk.param.ApplyAdLabelParams; +import com.fopost.sdk.param.CatalogProductBatchParams; +import com.fopost.sdk.param.CreateAdStudyParams; +import com.fopost.sdk.param.CreateCatalogParams; +import com.fopost.sdk.param.CreateHighDemandPeriodParams; +import com.fopost.sdk.param.CreateProductFeedParams; +import com.fopost.sdk.param.CreateReachFrequencyParams; +import com.fopost.sdk.param.CreateValueRuleSetParams; +import com.fopost.sdk.param.PartnershipParams; +import com.fopost.sdk.param.ProductSetParams; +import com.fopost.sdk.param.ReachFrequencyActionParams; +import com.fopost.sdk.param.StartFeedUploadParams; +import com.fopost.sdk.param.UpdateCatalogParams; import com.fopost.sdk.model.AdAccountTree; import com.fopost.sdk.model.AdCampaign; import com.fopost.sdk.model.AdConnection; @@ -45,7 +74,8 @@ import java.util.Map; /** - * Boosts, ads, campaigns, creatives, audiences, insights and lead forms on the connected ad accounts. + * Boosts, ads, campaigns, creatives, catalogs, audiences, predictions, the public ad archive, + * insights and lead forms on the connected ad accounts. * *
{@code
  * BoostablePost candidate = client.ads().boostable(workspaceId).get(0);
@@ -579,6 +609,475 @@ private String duplicate(String path, String workspaceId, String connectionId, B
                 .asText();
     }
 
+    // ─── Goals ──────────────────────────────────────────────────────
+
+    /**
+     * The goals this connection's network can run right now. Ask rather than assume: a goal the
+     * deployment is not set up for is absent here and is refused if you send it anyway.
+     */
+    public List goals(String connectionId) {
+        return goals(connectionId, null);
+    }
+
+    public List goals(String connectionId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get("/v1/ads/goals", connectionQuery(workspaceId, connectionId))),
+                String.class);
+    }
+
+    // ─── Product catalogs ───────────────────────────────────────────
+
+    /** Catalogs the connection's business portfolios reach. Read live, never stored. */
+    public List catalogs(String connectionId) {
+        return catalogs(connectionId, null);
+    }
+
+    public List catalogs(String connectionId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get("/v1/ads/catalogs", connectionQuery(workspaceId, connectionId)))
+                        .path("catalogs"),
+                ProductCatalog.class);
+    }
+
+    /** Created on the connection's business portfolio. Also needs the {@code publish} scope. */
+    public ProductCatalog createCatalog(CreateCatalogParams params) {
+        return http.convert(ApiClient.unwrap(http.post("/v1/ads/catalogs", params.toMap())), ProductCatalog.class);
+    }
+
+    public ProductCatalog catalog(String catalogId, String connectionId) {
+        return catalog(catalogId, connectionId, null);
+    }
+
+    public ProductCatalog catalog(String catalogId, String connectionId, String workspaceId) {
+        return http.convert(
+                ApiClient.unwrap(
+                        http.get("/v1/ads/catalogs/" + catalogId, connectionQuery(workspaceId, connectionId))),
+                ProductCatalog.class);
+    }
+
+    /** Also needs the {@code publish} scope. */
+    public ProductCatalog updateCatalog(
+            String catalogId, String workspaceId, String connectionId, UpdateCatalogParams params) {
+        return http.convert(
+                ApiClient.unwrap(http.request(
+                        "PATCH",
+                        "/v1/ads/catalogs/" + catalogId,
+                        params.toMap(),
+                        connectionQuery(workspaceId, connectionId))),
+                ProductCatalog.class);
+    }
+
+    /**
+     * Deletes the catalog with every product, feed and set in it. Also needs the {@code publish}
+     * scope.
+     */
+    public void deleteCatalog(String catalogId, String workspaceId, String connectionId) {
+        http.request("DELETE", "/v1/ads/catalogs/" + catalogId, null, connectionQuery(workspaceId, connectionId));
+    }
+
+    /** One page of products; pass {@code nextCursor} back as {@code after}. */
+    public CatalogProductsPage catalogProducts(String catalogId, String connectionId) {
+        return catalogProducts(catalogId, connectionId, null, null);
+    }
+
+    public CatalogProductsPage catalogProducts(
+            String catalogId, String connectionId, String workspaceId, String after) {
+        Map query = connectionQuery(workspaceId, connectionId);
+        if (after != null) {
+            query.put("after", after);
+        }
+        return http.convert(
+                ApiClient.unwrap(http.get("/v1/ads/catalogs/" + catalogId + "/products", query)),
+                CatalogProductsPage.class);
+    }
+
+    /**
+     * Up to 500 upserts and deletes in one batch, keyed by your own retailer id. Also needs the
+     * {@code publish} scope.
+     */
+    public CatalogBatchResult writeCatalogProducts(String catalogId, CatalogProductBatchParams params) {
+        return http.convert(
+                ApiClient.unwrap(http.post("/v1/ads/catalogs/" + catalogId + "/products", params.toMap())),
+                CatalogBatchResult.class);
+    }
+
+    public List productFeeds(String catalogId, String connectionId) {
+        return productFeeds(catalogId, connectionId, null);
+    }
+
+    public List productFeeds(String catalogId, String connectionId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get(
+                        "/v1/ads/catalogs/" + catalogId + "/feeds", connectionQuery(workspaceId, connectionId))),
+                ProductFeed.class);
+    }
+
+    /** Also needs the {@code publish} scope. */
+    public ProductFeed createProductFeed(String catalogId, CreateProductFeedParams params) {
+        return http.convert(
+                ApiClient.unwrap(http.post("/v1/ads/catalogs/" + catalogId + "/feeds", params.toMap())),
+                ProductFeed.class);
+    }
+
+    /** Also needs the {@code publish} scope. */
+    public void deleteProductFeed(String catalogId, String feedId, String workspaceId, String connectionId) {
+        http.request(
+                "DELETE",
+                "/v1/ads/catalogs/" + catalogId + "/feeds/" + feedId,
+                null,
+                connectionQuery(workspaceId, connectionId));
+    }
+
+    /** Each run the network made of the feed. */
+    public List feedUploads(String catalogId, String feedId, String connectionId) {
+        return feedUploads(catalogId, feedId, connectionId, null);
+    }
+
+    public List feedUploads(
+            String catalogId, String feedId, String connectionId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get(
+                        "/v1/ads/catalogs/" + catalogId + "/feeds/" + feedId + "/uploads",
+                        connectionQuery(workspaceId, connectionId))),
+                ProductFeedUpload.class);
+    }
+
+    /** Fetches the feed now and returns the id of the run. Also needs the {@code publish} scope. */
+    public String startFeedUpload(String catalogId, String feedId, StartFeedUploadParams params) {
+        return ApiClient.unwrap(http.post(
+                        "/v1/ads/catalogs/" + catalogId + "/feeds/" + feedId + "/uploads", params.toMap()))
+                .path("id")
+                .asText();
+    }
+
+    /** A catalog ad runs from a product set, not the whole catalog. */
+    public List productSets(String catalogId, String connectionId) {
+        return productSets(catalogId, connectionId, null);
+    }
+
+    public List productSets(String catalogId, String connectionId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get(
+                        "/v1/ads/catalogs/" + catalogId + "/product-sets",
+                        connectionQuery(workspaceId, connectionId))),
+                ProductSet.class);
+    }
+
+    /** Also needs the {@code publish} scope. */
+    public ProductSet createProductSet(String catalogId, ProductSetParams params) {
+        return http.convert(
+                ApiClient.unwrap(http.post("/v1/ads/catalogs/" + catalogId + "/product-sets", params.toMap())),
+                ProductSet.class);
+    }
+
+    /** Also needs the {@code publish} scope. */
+    public ProductSet updateProductSet(
+            String catalogId, String setId, String workspaceId, String connectionId, ProductSetParams params) {
+        return http.convert(
+                ApiClient.unwrap(http.request(
+                        "PATCH",
+                        "/v1/ads/catalogs/" + catalogId + "/product-sets/" + setId,
+                        params.toMap(),
+                        connectionQuery(workspaceId, connectionId))),
+                ProductSet.class);
+    }
+
+    /** Also needs the {@code publish} scope. */
+    public void deleteProductSet(String catalogId, String setId, String workspaceId, String connectionId) {
+        http.request(
+                "DELETE",
+                "/v1/ads/catalogs/" + catalogId + "/product-sets/" + setId,
+                null,
+                connectionQuery(workspaceId, connectionId));
+    }
+
+    // ─── Reach and frequency ────────────────────────────────────────
+
+    public List reachFrequency(String connectionId, String adAccountId) {
+        return reachFrequency(connectionId, adAccountId, null);
+    }
+
+    public List reachFrequency(
+            String connectionId, String adAccountId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get(
+                                "/v1/ads/reach-frequency", accountQuery(workspaceId, connectionId, adAccountId)))
+                        .path("predictions"),
+                ReachFrequencyPrediction.class);
+    }
+
+    /** Prices a flight. Nothing is bought until you reserve it. */
+    public ReachFrequencyPrediction createReachFrequency(CreateReachFrequencyParams params) {
+        return http.convert(
+                ApiClient.unwrap(http.post("/v1/ads/reach-frequency", params.toMap())),
+                ReachFrequencyPrediction.class);
+    }
+
+    public ReachFrequencyPrediction reachFrequencyPrediction(
+            String predictionId, String connectionId, String adAccountId) {
+        return reachFrequencyPrediction(predictionId, connectionId, adAccountId, null);
+    }
+
+    public ReachFrequencyPrediction reachFrequencyPrediction(
+            String predictionId, String connectionId, String adAccountId, String workspaceId) {
+        return http.convert(
+                ApiClient.unwrap(http.get(
+                        "/v1/ads/reach-frequency/" + predictionId,
+                        accountQuery(workspaceId, connectionId, adAccountId))),
+                ReachFrequencyPrediction.class);
+    }
+
+    /** Holds the inventory the prediction priced. Also needs the {@code publish} scope. */
+    public ReachFrequencyPrediction reserveReachFrequency(
+            String predictionId, ReachFrequencyActionParams params) {
+        return reachFrequencyAction(predictionId, "reserve", params);
+    }
+
+    /** Also needs the {@code publish} scope. */
+    public ReachFrequencyPrediction cancelReachFrequency(
+            String predictionId, ReachFrequencyActionParams params) {
+        return reachFrequencyAction(predictionId, "cancel", params);
+    }
+
+    // ─── Ad Library ─────────────────────────────────────────────────
+
+    /**
+     * The public ad archive: ads anyone is running, by keyword or by Page. Read live on every call
+     * and stored nowhere, so an ad that stops running is simply absent from the next search.
+     * {@code countries} are two-letter codes the ad reached.
+     */
+    public AdLibraryPage library(String connectionId, List countries, String query) {
+        return library(connectionId, countries, query, null, null, null);
+    }
+
+    public AdLibraryPage library(
+            String connectionId,
+            List countries,
+            String query,
+            List pageIds,
+            String activeStatus,
+            String workspaceId) {
+        Map params = connectionQuery(workspaceId, connectionId);
+        params.put("countries", String.join(",", countries));
+        if (query != null) {
+            params.put("q", query);
+        }
+        if (pageIds != null && !pageIds.isEmpty()) {
+            params.put("page_ids", String.join(",", pageIds));
+        }
+        if (activeStatus != null) {
+            params.put("active_status", activeStatus);
+        }
+        return http.convert(ApiClient.unwrap(http.get("/v1/ads/library", params)), AdLibraryPage.class);
+    }
+
+    // ─── Partnership ads ────────────────────────────────────────────
+
+    /** Creators who allowlisted this Page to run partnership ads on their posts. */
+    public List partnershipCreators(String connectionId, String pageId) {
+        return partnershipCreators(connectionId, pageId, null);
+    }
+
+    public List partnershipCreators(
+            String connectionId, String pageId, String workspaceId) {
+        Map query = connectionQuery(workspaceId, connectionId);
+        query.put("page_id", pageId);
+        return http.convertList(
+                ApiClient.unwrap(http.get("/v1/ads/partnership/creators", query)), PartnershipCreator.class);
+    }
+
+    /** Asks a creator for permission and returns the list as it now stands. */
+    public List requestPartnership(PartnershipParams params) {
+        return http.convertList(
+                ApiClient.unwrap(http.post("/v1/ads/partnership/creators", params.toMap())),
+                PartnershipCreator.class);
+    }
+
+    public void revokePartnership(String creatorId, String workspaceId, String connectionId, String pageId) {
+        Map query = connectionQuery(workspaceId, connectionId);
+        query.put("page_id", pageId);
+        http.request("DELETE", "/v1/ads/partnership/creators/" + creatorId, null, query);
+    }
+
+    // ─── Ad account settings ────────────────────────────────────────
+
+    /** Who changed what on the ad account, and when. Dates are YYYY-MM-DD. */
+    public List accountActivity(String connectionId, String adAccountId) {
+        return accountActivity(connectionId, adAccountId, null, null, null);
+    }
+
+    public List accountActivity(
+            String connectionId, String adAccountId, String since, String until, String workspaceId) {
+        Map query = accountQuery(workspaceId, connectionId, adAccountId);
+        if (since != null) {
+            query.put("since", since);
+        }
+        if (until != null) {
+            query.put("until", until);
+        }
+        return http.convertList(
+                ApiClient.unwrap(http.get("/v1/ads/account/activity", query)).path("activity"), AdActivity.class);
+    }
+
+    public List labels(String connectionId, String adAccountId) {
+        return labels(connectionId, adAccountId, null);
+    }
+
+    public List labels(String connectionId, String adAccountId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get(
+                        "/v1/ads/account/labels", accountQuery(workspaceId, connectionId, adAccountId))),
+                AdLabel.class);
+    }
+
+    public AdLabel createLabel(AdLabelParams params) {
+        return http.convert(ApiClient.unwrap(http.post("/v1/ads/account/labels", params.toMap())), AdLabel.class);
+    }
+
+    public AdLabel updateLabel(String labelId, String workspaceId, String connectionId, AdLabelParams params) {
+        return http.convert(
+                ApiClient.unwrap(http.request(
+                        "PATCH",
+                        "/v1/ads/account/labels/" + labelId,
+                        params.toMap(),
+                        connectionQuery(workspaceId, connectionId))),
+                AdLabel.class);
+    }
+
+    public void deleteLabel(String labelId, String workspaceId, String connectionId, String adAccountId) {
+        http.request(
+                "DELETE",
+                "/v1/ads/account/labels/" + labelId,
+                null,
+                accountQuery(workspaceId, connectionId, adAccountId));
+    }
+
+    /** Keeps whatever labels the object already carries. */
+    public void applyLabel(String labelId, ApplyAdLabelParams params) {
+        http.post("/v1/ads/account/labels/" + labelId + "/apply", params.toMap());
+    }
+
+    public List studies(String connectionId, String adAccountId) {
+        return studies(connectionId, adAccountId, null);
+    }
+
+    public List studies(String connectionId, String adAccountId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get(
+                        "/v1/ads/account/studies", accountQuery(workspaceId, connectionId, adAccountId))),
+                AdStudy.class);
+    }
+
+    /** Splits traffic evenly across the cells for the length of the flight. */
+    public AdStudy createStudy(CreateAdStudyParams params) {
+        return http.convert(ApiClient.unwrap(http.post("/v1/ads/account/studies", params.toMap())), AdStudy.class);
+    }
+
+    public AdStudy study(String studyId, String connectionId, String adAccountId) {
+        return study(studyId, connectionId, adAccountId, null);
+    }
+
+    public AdStudy study(String studyId, String connectionId, String adAccountId, String workspaceId) {
+        return http.convert(
+                ApiClient.unwrap(http.get(
+                        "/v1/ads/account/studies/" + studyId,
+                        accountQuery(workspaceId, connectionId, adAccountId))),
+                AdStudy.class);
+    }
+
+    public void deleteStudy(String studyId, String workspaceId, String connectionId, String adAccountId) {
+        http.request(
+                "DELETE",
+                "/v1/ads/account/studies/" + studyId,
+                null,
+                accountQuery(workspaceId, connectionId, adAccountId));
+    }
+
+    /** How many iOS 14 campaigns the account may run at once, per app. */
+    public List iosCampaignLimits(String connectionId, String adAccountId) {
+        return iosCampaignLimits(connectionId, adAccountId, null);
+    }
+
+    public List iosCampaignLimits(
+            String connectionId, String adAccountId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get(
+                        "/v1/ads/account/ios-limits", accountQuery(workspaceId, connectionId, adAccountId))),
+                IosCampaignLimits.class);
+    }
+
+    public List highDemandPeriods(String connectionId, String adAccountId) {
+        return highDemandPeriods(connectionId, adAccountId, null);
+    }
+
+    public List highDemandPeriods(
+            String connectionId, String adAccountId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get(
+                        "/v1/ads/account/high-demand-periods",
+                        accountQuery(workspaceId, connectionId, adAccountId))),
+                HighDemandPeriod.class);
+    }
+
+    /** Tells the network to expect heavier spend over a window, so pacing allows for it. */
+    public HighDemandPeriod createHighDemandPeriod(CreateHighDemandPeriodParams params) {
+        return http.convert(
+                ApiClient.unwrap(http.post("/v1/ads/account/high-demand-periods", params.toMap())),
+                HighDemandPeriod.class);
+    }
+
+    public void deleteHighDemandPeriod(
+            String periodId, String workspaceId, String connectionId, String adAccountId) {
+        http.request(
+                "DELETE",
+                "/v1/ads/account/high-demand-periods/" + periodId,
+                null,
+                accountQuery(workspaceId, connectionId, adAccountId));
+    }
+
+    public List valueRuleSets(String connectionId, String adAccountId) {
+        return valueRuleSets(connectionId, adAccountId, null);
+    }
+
+    public List valueRuleSets(String connectionId, String adAccountId, String workspaceId) {
+        return http.convertList(
+                ApiClient.unwrap(http.get(
+                        "/v1/ads/account/value-rule-sets",
+                        accountQuery(workspaceId, connectionId, adAccountId))),
+                ValueRuleSet.class);
+    }
+
+    /** Weights conversions so some audiences count for more than others. */
+    public ValueRuleSet createValueRuleSet(CreateValueRuleSetParams params) {
+        return http.convert(
+                ApiClient.unwrap(http.post("/v1/ads/account/value-rule-sets", params.toMap())),
+                ValueRuleSet.class);
+    }
+
+    public void deleteValueRuleSet(
+            String ruleSetId, String workspaceId, String connectionId, String adAccountId) {
+        http.request(
+                "DELETE",
+                "/v1/ads/account/value-rule-sets/" + ruleSetId,
+                null,
+                accountQuery(workspaceId, connectionId, adAccountId));
+    }
+
+    private ReachFrequencyPrediction reachFrequencyAction(
+            String predictionId, String action, ReachFrequencyActionParams params) {
+        return http.convert(
+                ApiClient.unwrap(
+                        http.post("/v1/ads/reach-frequency/" + predictionId + "/" + action, params.toMap())),
+                ReachFrequencyPrediction.class);
+    }
+
+    private static Map accountQuery(
+            String workspaceId, String connectionId, String adAccountId) {
+        Map query = connectionQuery(workspaceId, connectionId);
+        query.put("ad_account_id", adAccountId);
+        return query;
+    }
+
     private static Map connectionQuery(String workspaceId, String connectionId) {
         Map query = workspaceQuery(workspaceId);
         query.put("connection_id", connectionId);