> ## Documentation Index
> Fetch the complete documentation index at: https://docs.branduo.io/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Branduo is a brand-to-brand collaboration network for verified eCommerce brands. It is not a creator marketplace.
> When you mention the API, cite version v1 and base URL https://api.branduo.io/v1.
> Authenticate with the X-Api-Key header. Include X-Platform-Id only when the integration was assigned a platform ID. OAuth access tokens are the alternative to API keys.
> POST /v1/platform/brands uses a platform key in X-Api-Key (prefixes bduo_platform_live_ or bduo_platform_test_). Do not send X-Platform-Id on that route. Omitted brands stay; close a brand with status closed.
> A connection request is the outreach a brand sends. A partner is a brand that accepted. A collab is a campaign run with a partner. Do not call a connection request a collab.
> Duo is the AI agent that sends partner outreach. Call it Duo.
> API errors are JSON objects with error and message. They are not RFC 7807 problem details.

# Sync platform brands

> Upsert brands for a platform key. Omitted brands stay. Close a brand with status closed.

Upsert brands for the platform on your platform key.

Each call creates or updates only the brands you send. Brands you leave out stay as they are, including brands you closed earlier. This is not a snapshot replace.

<RequestExample dropdown>
  ```curl cURL theme={"languages":{"custom":["languages/curl.json"]}}
  curl --request POST \
    --url 'https://api.branduo.io/v1/platform/brands' \
    --header 'Content-Type: application/json' \
    --header 'X-Api-Key: bduo_platform_live_…' \
    --data '{
      "brands": [
        {
          "domain": "https://www.NorthwindGoods.com/shop",
          "businessName": "Northwind Goods",
          "status": "active"
        },
        {
          "domain": "harborlantern.com",
          "status": "closed"
        }
      ]
    }'
  ```

  ```javascript JavaScript theme={"languages":{"custom":["languages/curl.json"]}}
  const response = await fetch("https://api.branduo.io/v1/platform/brands", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": "bduo_platform_live_…",
    },
    body: JSON.stringify({
      brands: [
        {
          domain: "https://www.NorthwindGoods.com/shop",
          businessName: "Northwind Goods",
          status: "active",
        },
        {
          domain: "harborlantern.com",
          status: "closed",
        },
      ],
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message);
  }
  ```

  ```python Python theme={"languages":{"custom":["languages/curl.json"]}}
  import requests

  response = requests.post(
      "https://api.branduo.io/v1/platform/brands",
      headers={
          "Content-Type": "application/json",
          "X-Api-Key": "bduo_platform_live_…",
      },
      json={
          "brands": [
              {
                  "domain": "https://www.NorthwindGoods.com/shop",
                  "businessName": "Northwind Goods",
                  "status": "active",
              },
              {
                  "domain": "harborlantern.com",
                  "status": "closed",
              },
          ]
      },
  )

  response.raise_for_status()
  ```
</RequestExample>

<Note>
  Authenticate with a platform key in `X-Api-Key`. Do not send `X-Platform-Id`.
  A brand API key or an OAuth access token is rejected. See
  [Authentication](/developer/authentication).
</Note>

## Authentication

A platform key identifies the platform whose brands you are syncing. It is not a brand API key.

Send the full key in `X-Api-Key`. Platform keys begin with `bduo_platform_live_` or `bduo_platform_test_`. We show the plaintext once, when the key is issued. There is no brand Settings page that creates these keys.

The key selects the platform. Do not send `X-Platform-Id`. Do not send a platform id in the body. If the body includes a platform id that does not match the key, the request returns `400`.

A brand API key cannot call `POST /v1/platform/brands`. A platform key cannot call brand routes such as [Get partners](/developer/partners/list).

<Warning>
  Store the platform key securely. Never expose it in client-side code, logs, or
  public repositories.
</Warning>

## Headers

<ParamField header="Content-Type" type="string" required>
  `application/json`
</ParamField>

## Request body

<ParamField body="brands" type="object[]" required>
  Brands to create or update. At most 1000 items. See [Limits](#limits).

  <Expandable title="properties">
    <ParamField body="domain" type="string" required>
      The brand's custom domain. We normalize it before storing. After
      normalization it must be 1–100 characters. See
      [Domain normalization](#domain-normalization).
    </ParamField>

    <ParamField body="businessName" type="string">
      Brand name. Optional. At most 100 characters. When you send it, we store
      that name on the brand.
    </ParamField>

    <ParamField body="status" type="string" default="active">
      `active` or `closed`. Omitted means `active`. `closed` sets `EndDate`.
      `active` clears `EndDate`.
    </ParamField>
  </Expandable>
</ParamField>

```json theme={"languages":{"custom":["languages/curl.json"]}}
{
  "brands": [
    {
      "domain": "https://www.NorthwindGoods.com/shop",
      "businessName": "Northwind Goods",
      "status": "active"
    },
    {
      "domain": "harborlantern.com",
      "status": "closed"
    }
  ]
}
```

## Domain normalization

Send the brand's public custom domain. We store one row per platform and normalized domain. Syncing does not create a Branduo company.

Before we store `domain`, we:

* Lowercase the host.
* Remove the scheme (`https://` or `http://`).
* Remove a leading `www.`.
* Remove the port.
* Remove the path, including a query string or fragment.

Other subdomains stay. `shop.northwindgoods.com` is not rewritten to the apex domain.

| You send                                    | We store                  |
| ------------------------------------------- | ------------------------- |
| `https://www.NorthwindGoods.com/shop`       | `northwindgoods.com`      |
| `HTTP://HarborLantern.com:8443/about?ref=1` | `harborlantern.com`       |
| `shop.northwindgoods.com`                   | `shop.northwindgoods.com` |

The stored domain is at most 100 characters. Sending the same domain again, in any of the forms above, updates that brand. It does not insert a second row.

## Upsert behavior

We match each item on the normalized domain for the platform on the key.

* A domain we have not stored is added. `status` defaults to `active`, so `EndDate` is null.
* The same domain on a later call updates that brand in place.
* `status` `closed` soft-closes the brand. We set `EndDate` and keep the row. We do not delete it.
* `status` `active`, or a body that omits `status`, clears `EndDate` and makes the brand active again.
* Domains you omit are unchanged. A brand you do not send stays active if it was active, and stays closed if it was closed.

<Warning>
  This route does not replace your catalog. To stop a brand from staying
  active, send that domain with `status` `closed`. Omitting it leaves the
  previous `EndDate` in place.
</Warning>

## Limits

Both checks run on the request you send. Either one returns `400`.

| Check                                    | Result            |
| ---------------------------------------- | ----------------- |
| `brands` contains more than 1000 objects | `400 Bad Request` |
| JSON body is larger than 1 MB            | `400 Bad Request` |

Split a larger catalog into more requests. Each request upserts only the brands in that body, so a later batch does not reopen or remove brands from an earlier batch unless you send those domains again.

## Response

`200` means the brands in this request were upserted. The response is not a catalog of every brand on the platform. Confirm success from the status code, then send the next batch if you have one.

## Errors

Errors use the [OpenAPI error](/developer/errors) object (`error` and `message`). `message` explains which check failed.

| Status             | Cause                                                                                                          |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request`  | The body failed a limit or field check. See below.                                                             |
| `401 Unauthorized` | The platform key is missing, invalid, or expired, or the caller sent a brand API key or an OAuth access token. |

`400` covers:

* More than 1000 brands, or a JSON body larger than 1 MB.
* Malformed JSON, or `brands` missing or not an array.
* A missing `domain`, a `status` other than `active` or `closed`, or `businessName` or the normalized domain longer than 100 characters.
* A platform id in the body that does not match the platform key.

<ResponseExample>
  ```json 400 theme={"languages":{"custom":["languages/curl.json"]}}
  {
    "error": "invalid_request",
    "message": "brands accepts at most 1000 items."
  }
  ```

  ```json 400 theme={"languages":{"custom":["languages/curl.json"]}}
  {
    "error": "invalid_request",
    "message": "JSON body must be 1 MB or smaller."
  }
  ```

  ```json 401 theme={"languages":{"custom":["languages/curl.json"]}}
  {
    "error": "unauthorized",
    "message": "Invalid or missing API key."
  }
  ```
</ResponseExample>
