← All posts

Amazon Ads API v1: A New Unified Approach — Notes from a New York Meetup

Last month I was in New York for a series of meetings that included a tech gathering where several Amazon Ads engineers were presenting the direction of their advertising API. I had been aware of the "Amazon Ads API v1" project for a while but had not fully understood its scope. After that evening, I left with a clear picture of what it is, why it matters, and the migration work we need to plan at the platform.

Context: The Problem With the Current API Landscape

If you have built tools on top of the Amazon Advertising API, you know the pain. Sponsored Products, Sponsored Brands, Sponsored Display, and DSP are all separate product lines — and historically they each have their own API surface, their own endpoint naming conventions, their own request/response shapes, and their own error formats.

Want to create a campaign? The Sponsored Products endpoint and the Sponsored Display endpoint have different request schemas. Want to list ad groups? Different pagination implementations. Want to handle errors? Different error code vocabularies. Building a multi-product campaign management tool means maintaining separate code paths for each ad type, which is exactly the situation we are in at the platform.

What Is the New Amazon Ads API v1?

The new Amazon Ads API v1 is described in their documentation as: "A reimagined approach to our Ads API, designed to provide a seamless experience across all Amazon advertising products through a common model."

This is not a minor API revision. It is a ground-up redesign with a unified data model that applies consistently across all advertising products. The Amazon engineers at the meetup were explicit: if you are building a new integration today, start with v1. The legacy endpoints (what most of us have been using) will continue to work for a transition period, but v1 is where new capabilities will be added.

The Common Model: What Changes

The biggest shift is that campaign lifecycle operations — create, update, pause, resume, archive — now follow the same request/response contract regardless of which ad product you are working with. Here is what campaign creation looks like in v1:

// v1 Unified campaign creation — same shape for SP, SB, SD
POST /campaigns
Authorization: Bearer {token}
Amazon-Advertising-API-ClientId: {clientId}
Amazon-Advertising-API-Scope: {profileId}

{
  "name": "Summer Sale - Go SDK Launch",
  "adProduct": "SPONSORED_PRODUCTS",
  "targetingType": "MANUAL",
  "budget": {
    "budgetType": "DAILY",
    "budget": 75.0,
    "currencyCode": "USD"
  },
  "startDate": "2025-12-10",
  "state": "PAUSED"
}

Compare this to the legacy Sponsored Products endpoint:

// Legacy SP campaign creation — different schema
POST /v2/sp/campaigns

[{
  "name": "Summer Sale - Go SDK Launch",
  "campaignType": "sponsoredProducts",
  "targetingType": "manual",
  "state": "paused",
  "dailyBudget": 75.0,
  "startDate": "20251210"  // note: different date format!
}]

The differences seem cosmetic at first glance, but at scale they matter. Standardised date formats, consistent field naming (camelCase vs mixed), a proper budget object instead of a flat field, and a single endpoint instead of product-specific paths.

Unified Targeting Objects

One of the most painful inconsistencies in the legacy API is that keyword targeting for Sponsored Products works completely differently from audience targeting for Sponsored Display. In v1, targeting follows a common model with a discriminated union structure:

{
  "targets": [{
    "targetType": "KEYWORD",
    "keyword": {
      "text": "go programming book",
      "matchType": "BROAD"
    }
  }, {
    "targetType": "AUDIENCE",
    "audience": {
      "audienceId": "aud-123",
      "bid": 1.25
    }
  }]
}

For a tool like ours that manages both keyword-targeted SP campaigns and audience-targeted SD campaigns for the same advertiser, having a common targeting model means a single code path instead of two parallel implementations.

RFC 7807 Error Responses

The legacy API's error handling is inconsistent. Sometimes an HTTP 400 with a code, sometimes a 200 with an error array embedded in the response body. In v1, errors follow RFC 7807 Problem Details:

{
  "type": "https://advertising.amazon.com/errors/BUDGET_BELOW_MINIMUM",
  "title": "Campaign budget is below the minimum allowed",
  "detail": "Daily budget must be at least $1.00 for Sponsored Products in the US marketplace",
  "instance": "/campaigns/req_xxxxxxxx",
  "extensions": {
    "minimumBudget": 1.00,
    "currency": "USD"
  }
}

Machine-readable type URIs, human-readable detail strings, and extension fields for structured error data. Error handling in client code becomes straightforward:

type APIError struct {
    Type       string          `json:"type"`
    Title      string          `json:"title"`
    Detail     string          `json:"detail"`
    Instance   string          `json:"instance"`
    Extensions json.RawMessage `json:"extensions"`
}

func (e *APIError) Is(target error) bool {
    if t, ok := target.(*APIError); ok {
        return strings.HasSuffix(e.Type, t.Title) // match by error type suffix
    }
    return false
}

var ErrBudgetBelowMinimum = &APIError{Type: "BUDGET_BELOW_MINIMUM"}

// Usage:
if errors.Is(err, ErrBudgetBelowMinimum) {
    // Handle specifically
}

What the Engineers Said About Timeline

At the meetup, the Amazon Ads team was careful not to commit to a deprecation date for the legacy API. Their message was clear: the legacy endpoints are not going away anytime soon, but all new features and capabilities will be built exclusively on v1. If you want access to new ad products, new targeting options, or new reporting capabilities as they are released, you need to be on v1.

My read: plan for a 12–18 month migration window. There is no fire, but there is a clear direction.

Migration Strategy

We manage thousands of active campaigns for hundreds of advertisers. Migrating in one shot is not feasible. Our approach:

  1. New integrations start on v1: any new ad product support we build (we are currently evaluating Sponsored TV) goes directly into the v1 API surface.
  2. v1 client in parallel: build the v1 Go client alongside the legacy one. They share authentication but have separate request/response types.
  3. Shadow mode: for a period, send the same operations to both legacy and v1. Compare responses. Build confidence that v1 returns equivalent data.
  4. Feature-by-feature cutover: migrate one operation at a time, starting with read operations (less risk than writes).

The Go Client Skeleton

type V1Client struct {
    http      *http.Client
    baseURL   string
    tokens    *TokenCache
}

func (c *V1Client) CreateCampaign(ctx context.Context, profileID string, req CreateCampaignRequest) (*Campaign, error) {
    body, _ := json.Marshal(req)
    httpReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/campaigns", bytes.NewReader(body))
    httpReq.Header.Set("Amazon-Advertising-API-Scope", profileID)
    httpReq.Header.Set("Authorization", "Bearer "+c.tokens.Must(ctx, profileID))
    httpReq.Header.Set("Content-Type", "application/json")

    resp, err := c.http.Do(httpReq)
    if err != nil { return nil, err }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusCreated {
        var apiErr APIError
        json.NewDecoder(resp.Body).Decode(&apiErr)
        return nil, &apiErr
    }

    var campaign Campaign
    json.NewDecoder(resp.Body).Decode(&campaign)
    return &campaign, nil
}

The evening in New York was a useful reminder that the tooling we build on top of external APIs has a shelf life determined by the API provider's roadmap. The shift to v1 is well-designed and the migration work is real but manageable. Worth starting now rather than waiting until the legacy API has a retirement date attached to it.

Comments