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

# Digital Services Act (DSA) Setup

> Set up billing contacts so every campaign carries the advertiser and payer details the EU Digital Services Act requires you to show next to each ad.

export const IntegrationsCard1A = () => {
  return <>
      <style>{`
        .integrations-card-1a {
          display: flex;
          flex-direction: column;
          background-color: #f0f2f4;
          border: 1px solid rgba(0, 0, 0, 0.08);
          border-radius: 12px;
          padding: 1.25rem 1.5rem;
          gap: 0.25rem;
          color: #25253b;
          box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
          transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
          margin-top: 2rem;
        }

        .integrations-card-1a:hover {
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
          border-color: rgba(0, 0, 0, 0.12);
        }

        .dark .integrations-card-1a {
          background-color: #25253b;
          color: #f6f8fa;
          border-color: rgba(255, 255, 255, 0.08);
          box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        }

        .dark .integrations-card-1a:hover {
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
          border-color: rgba(255, 255, 255, 0.12);
        }

        .integrations-card-1a-avatar-row {
          display: flex;
          margin-left: 14px;
          flex-wrap: nowrap;
        }

        .integrations-card-1a-avatar {
          width: 44px;
          height: 44px;
          border-radius: 50%;
          border: 2px solid #f0f2f4;
          margin-left: -14px;
          object-fit: cover;
          box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
          pointer-events: none;
          cursor: default;
        }

        .dark .integrations-card-1a-avatar {
          border-color: #25253b;
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
        }

        .integrations-card-1a h2 {
          margin: 0;
          line-height: 1.25;
          font-size: clamp(1.25rem, calc(0.875rem + 1.25vw), 1.75rem);
          font-weight: 700;
          letter-spacing: -0.025em;
          color: #111827;
        }

        .dark .integrations-card-1a h2 {
          color: #f9fafb;
        }

        .integrations-card-1a-subtitle {
          font-size: 0.9375rem;
          font-weight: 500;
          line-height: 1.5;
          color: #4b5563;
          margin: 0;
        }

        .dark .integrations-card-1a-subtitle {
          color: #d1d5db;
        }

        .integrations-card-1a p {
          margin: 0;
          line-height: 1.5;
          font-size: 0.875rem;
          color: #374151;
        }

        .dark .integrations-card-1a p {
          color: #d1d5db;
        }

        .integrations-card-1a a {
          color: #0500ff;
          text-decoration: none;
          font-weight: 500;
          transition: color 0.15s ease;
        }

        .integrations-card-1a a:hover {
          color: #0400cc;
        }

        .dark .integrations-card-1a a {
          color: #8280ff;
        }

        .dark .integrations-card-1a a:hover {
          color: #a19fff;
        }

        @media (max-width: 640px) {
          .integrations-card-1a-avatar {
            width: 38px;
            height: 38px;
            margin-left: -12px;
          }

          .integrations-card-1a-avatar-row {
            margin-left: 12px;
          }
        }

        @media (min-width: 1280px) {
          .integrations-card-1a p {
            font-size: 0.6875rem;
            line-height: 1.4;
            letter-spacing: -0.01em;
          }

          .integrations-card-1a h2 {
            font-size: clamp(1rem, calc(0.75rem + 1vw), 1.25rem);
          }

          .integrations-card-1a-subtitle {
            font-size: 0.75rem;
          }
        }
      `}</style>
      <div className="integrations-card-1a">
        <div className="integrations-card-1a-avatar-row">
          <img src="/images/integration-team/agustin.webp" alt="Agustin - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/amelia.webp" alt="Amelia - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/anais.webp" alt="Anais - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/bernardita.webp" alt="Bernardita - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/felipe.webp" alt="Felipe - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/gui.webp" alt="Gui - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/javiera.webp" alt="Javiera - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/linn.webp" alt="Linn - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/marcio.webp" alt="Marcio - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/max.webp" alt="Max - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/natalie.webp" alt="Natalie - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/rodrigo.webp" alt="Rodrigo - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
          <img src="/images/integration-team/tomas.webp" alt="Tomás - Integrations Team Member" loading="lazy" className="integrations-card-1a-avatar" />
        </div>

        <h2>Need help with your Topsort integration?</h2>

        <p className="integrations-card-1a-subtitle">
          Talk to us, the integrations engineering team.
        </p>

        <p>
          Reach us via <strong>your dedicated messaging channel</strong> or at{" "}
          <a href="mailto:support@topsort.com">support@topsort.com</a>.
        </p>

        <p>
          If you're not a customer yet,{" "}
          <a href="https://www.topsort.com/book-a-demo">book a demo</a> with our
          sales team instead.
        </p>
      </div>
    </>;
};

The EU **Digital Services Act (DSA)** requires online platforms that show ads to users in the European Union to tell users, for each ad and in real time:

* that the content is an advertisement;
* on whose behalf the ad is presented;
* who paid for the ad, when that is someone different;
* the main parameters used to decide who sees the ad.

Topsort stores the second and third items for you as a **billing contact** attached to each campaign. Your marketplace creates the billing contacts and links them to vendors, advertisers pick one when they create a campaign, and your site or app reads it back when it renders the ad.

<Note>
  This guide covers the data Topsort stores and how to get it to your storefront. Your legal team decides which details you must display and how. For ad labeling, see [Going Live](/en/overview/going-live).
</Note>

## How It Works

<Steps>
  <Step title="Create billing contacts">
    Your marketplace creates one billing contact per advertiser and payer combination, using the [Billing API](/en/api-reference/billing-api).
  </Step>

  <Step title="Link contacts to vendors">
    Each billing contact is linked to the vendors it can be used for. Only linked contacts appear in the vendor's campaign forms.
  </Step>

  <Step title="Assign a contact to each campaign">
    Advertisers select a billing contact when they create a campaign in the Topsort UI. Campaigns created through the API get one assigned with a separate call.
  </Step>

  <Step title="Show the disclosure with the ad">
    Auction winners include a `campaignId`. When a shopper clicks to see the ad's details, for example an "About this ad" link, your backend fetches that campaign's billing contact on demand and your storefront displays it.
  </Step>
</Steps>

All billing contact endpoints use your **Advanced API key**. See [API Key Types](/en/api-reference/authentication/api-key-types).

## The Billing Contact

A billing contact has an ID and three required text fields:

| Field | What it holds | Typical DSA use |
| - | - | - |
| ID | Your own identifier for the contact, set in the request path. | Not displayed. |
| `advertiserName` | The agency or advertiser that runs the campaign. This is the name advertisers see when they choose a contact in the campaign form. | Who manages the campaign. |
| `brandName` | The brand the ad promotes. | On whose behalf the ad is presented. |
| `invoiceTo` | The company responsible for paying the invoice. | Who paid for the ad. |

How billing contacts relate to vendors and campaigns:

* **You choose the ID.** IDs are unique within your marketplace and case-sensitive. Sending a request with an existing ID updates that contact.
* **Contacts and vendors are many-to-many.** One contact can be linked to several vendors, for example an agency that buys ads for more than one seller. One vendor can have several contacts, for example one per brand it sells.
* **A campaign has one billing contact at a time.** Assigning a new one replaces the previous one.

## Step 1: Create Billing Contacts

Call [Upsert Billing Contact](/en/api-reference/billing-api/upsert-billing-contact) with the ID you want to use:

```bash theme={null}
curl -X PUT https://api.topsort.com/public/v1/billing-service/billing-contacts/acme-media-brightsmile \
  -H "Authorization: Bearer YOUR_ADVANCED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "advertiserName": "Acme Media Agency",
    "brandName": "BrightSmile",
    "invoiceTo": "BrightSmile Europe GmbH"
  }'
```

A successful call returns `204 No Content`. Every call must include all three fields, because the request replaces the stored contact.

<Tip>
  Use readable, stable IDs from your own system, such as an advertiser account number. You need the ID again in every later step.
</Tip>

## Step 2: Link Contacts to Vendors

Call [Upsert Vendor Billing Contact](/en/api-reference/billing-api/upsert-vendor-billing-contact) once for each vendor that can use the contact:

```bash theme={null}
curl -X PUT https://api.topsort.com/public/v1/billing-service/vendor-billing-contact \
  -H "Authorization: Bearer YOUR_ADVANCED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "billingContactId": "acme-media-brightsmile",
    "vendorId": "vendor-123"
  }'
```

* `vendorId` is your vendor ID, the same one you send in your catalog.
* The billing contact must already exist. Otherwise the call returns `404`.
* Linking a contact that is already linked does nothing, so retries are safe.

To confirm which contacts a vendor has, call [Get Billing Contacts](/en/api-reference/billing-api/get-billing-contacts) with the vendor ID:

```bash theme={null}
curl "https://api.topsort.com/public/v1/billing-service/billing-contacts?external_vendor_id=vendor-123" \
  -H "Authorization: Bearer YOUR_ADVANCED_API_KEY"
```

```json theme={null}
{
  "billingContacts": [
    {
      "id": "acme-media-brightsmile",
      "advertiserName": "Acme Media Agency",
      "brandName": "BrightSmile",
      "invoiceTo": "BrightSmile Europe GmbH"
    }
  ],
  "hasMore": false,
  "nextPage": null
}
```

Leave out `external_vendor_id` to list every billing contact in your marketplace. Results come in pages of up to 100 (50 by default). When `hasMore` is `true`, send the `nextPage` value as the `next_page` query parameter to get the next page.

## Step 3: Assign a Billing Contact to Each Campaign

<Tabs>
  <Tab title="Topsort UI">
    When a vendor has at least one linked billing contact, the campaign creation form shows a **Billing Contact** field in the launch step. The list contains only that vendor's contacts, labeled by `advertiserName`.

    * **Sponsored Listings** campaigns require a billing contact once the vendor has one.
    * On other campaign types the field can be left empty. Use [Step 5](#step-5-check-coverage) to find campaigns without one.

    Advertisers can change the billing contact later by editing the campaign.

    <Tip>
      Give each contact a distinct `advertiserName`. Two contacts with the same advertiser name look identical in the list.
    </Tip>
  </Tab>

  <Tab title="Campaign API">
    [Create Campaign](/en/api-reference/campaign-api/create-campaign) does not accept a billing contact. After you create the campaign, assign one with [Upsert Campaign Billing Contact](/en/api-reference/billing-api/upsert-campaign-billing-contact):

    ```bash theme={null}
    curl -X PUT https://api.topsort.com/public/v1/billing-service/campaign-billing-contact \
      -H "Authorization: Bearer YOUR_ADVANCED_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "billingContactId": "acme-media-brightsmile",
        "campaignId": "0193f2a1-7c4e-7d3a-9b2f-5e8c1a6d4f20"
      }'
    ```

    * `campaignId` is the Topsort campaign ID returned when the campaign is created.
    * Calling this again for the same campaign replaces its billing contact.
    * Assign a contact that is linked to the campaign's vendor. The API does not check this, but the Topsort UI flags the mismatch when the campaign is edited.
  </Tab>
</Tabs>

## Step 4: Show the Disclosure With Each Ad

Sponsored Listings and banner winners in the [auction response](/en/api-reference/auctions/create-auctions) include the `campaignId` of the winning campaign. Keep it with each ad you render.

You don't need the billing contact to render the ad. Fetch it on demand: when a shopper clicks to see the ad's details, for example through an "About this ad" link, your backend calls [Get Campaign Billing Contact](/en/api-reference/billing-api/get-campaign-billing-contact) with that `campaignId`:

```bash theme={null}
curl "https://api.topsort.com/public/v1/billing-service/campaign-billing-contact?campaign_id=0193f2a1-7c4e-7d3a-9b2f-5e8c1a6d4f20" \
  -H "Authorization: Bearer YOUR_ADVANCED_API_KEY"
```

```json theme={null}
{
  "id": "acme-media-brightsmile",
  "advertiserName": "Acme Media Agency",
  "brandName": "BrightSmile",
  "invoiceTo": "BrightSmile Europe GmbH"
}
```

Then show the details your legal team chose in the ad's details panel. For example:

> **Sponsored** · Advertiser: BrightSmile · Paid by: BrightSmile Europe GmbH

Build the lookup with these rules:

* **Fetch on demand.** Call the endpoint when the shopper opens the ad's details, not while you render ads or for every impression.
* **Call it from your backend.** The Advanced API key must never reach a browser or mobile app. Your storefront calls your own backend endpoint, which calls Topsort.
* **Cache by campaign ID (optional).** Many shoppers may open details for ads from the same campaign. A short cache, a few minutes, avoids repeat calls and keeps the details current. Don't cache `404` responses, so a contact you assign later shows up right away.
* **Handle `404`.** A `404` means the campaign has no billing contact. Show a fallback, such as the vendor name, and fix the campaign as described in Step 5.

```typescript theme={null}
type BillingContact = {
  id: string;
  advertiserName: string;
  brandName: string;
  invoiceTo: string;
};

const cache = new Map<string, { contact: BillingContact; expiresAt: number }>();
const TTL_MS = 5 * 60 * 1000;

async function getBillingContact(campaignId: string): Promise<BillingContact | null> {
  const cached = cache.get(campaignId);
  if (cached && cached.expiresAt > Date.now()) return cached.contact;

  const res = await fetch(
    `https://api.topsort.com/public/v1/billing-service/campaign-billing-contact?campaign_id=${campaignId}`,
    { headers: { Authorization: `Bearer ${process.env.TOPSORT_ADVANCED_API_KEY}` } },
  );
  // Not cached, so a contact assigned later shows up on the next click.
  if (res.status === 404) return null;
  if (!res.ok) throw new Error(`Billing API error: ${res.status}`);

  const contact = (await res.json()) as BillingContact;
  cache.set(campaignId, { contact, expiresAt: Date.now() + TTL_MS });
  return contact;
}
```

<Note>
  Sponsored Brands winners do not include a `campaignId`. If you run Sponsored Brands in the EU, contact your Topsort account team.
</Note>

## Step 5: Check Coverage

Before you go live in the EU, and periodically after that, make sure every active campaign has a billing contact:

1. List your campaigns with [Get Campaigns](/en/api-reference/campaign-api/get-campaigns).
2. For each campaign, call [Get Campaign Billing Contact](/en/api-reference/billing-api/get-campaign-billing-contact).
3. For every `404`, assign a contact with [Upsert Campaign Billing Contact](/en/api-reference/billing-api/upsert-campaign-billing-contact).

To find vendors that can't pick a contact yet, call [Get Billing Contacts](/en/api-reference/billing-api/get-billing-contacts) with each vendor's ID. An empty `billingContacts` list means the vendor needs a contact linked (Step 2).

## Managing Billing Contacts Over Time

**Correcting a contact.** Send [Upsert Billing Contact](/en/api-reference/billing-api/upsert-billing-contact) again with the same ID. The change applies immediately to every campaign that uses the contact.

**When the advertiser or payer changes.** Create a new billing contact and assign it to the affected campaigns, instead of editing the existing one. Editing would change the details shown for every campaign that still uses the old contact.

**Unlinking a contact from a vendor.** Call [Delete Vendor Billing Contact](/en/api-reference/billing-api/delete-vendor-billing-contact):

```bash theme={null}
curl -X DELETE https://api.topsort.com/public/v1/billing-service/vendors/vendor-123/vendor-billing-contact/acme-media-brightsmile \
  -H "Authorization: Bearer YOUR_ADVANCED_API_KEY"
```

This removes the contact from the vendor's campaign forms. Campaigns that already use it keep it. To change those campaigns, assign them a different contact.

<Note>
  Billing contacts can't be deleted, and a campaign's billing contact can't be removed, only replaced.
</Note>

## Endpoint Reference

| Method | Endpoint | Purpose |
| - | - | - |
| `PUT` | [`/billing-contacts/{billing_contact_id}`](/en/api-reference/billing-api/upsert-billing-contact) | Create or update a billing contact |
| `GET` | [`/billing-contacts/{billing_contact_id}`](/en/api-reference/billing-api/get-billing-contact) | Get one billing contact |
| `GET` | [`/billing-contacts`](/en/api-reference/billing-api/get-billing-contacts) | List billing contacts, optionally for one vendor |
| `PUT` | [`/vendor-billing-contact`](/en/api-reference/billing-api/upsert-vendor-billing-contact) | Link a billing contact to a vendor |
| `DELETE` | [`/vendors/{external_vendor_id}/vendor-billing-contact/{billing_contact_id}`](/en/api-reference/billing-api/delete-vendor-billing-contact) | Unlink a billing contact from a vendor |
| `PUT` | [`/campaign-billing-contact`](/en/api-reference/billing-api/upsert-campaign-billing-contact) | Assign a billing contact to a campaign |
| `GET` | [`/campaign-billing-contact?campaign_id=`](/en/api-reference/billing-api/get-campaign-billing-contact) | Get a campaign's billing contact |

All paths are relative to `https://api.topsort.com/public/v1/billing-service`.

## Troubleshooting

<AccordionGroup>
  <Accordion title="The Billing Contact field doesn't appear in the campaign form">
    The vendor has no linked billing contacts. Check with [Get Billing Contacts](/en/api-reference/billing-api/get-billing-contacts) and the vendor's ID, then link a contact (Step 2). If the vendor has contacts and the field still doesn't appear, contact your Topsort account team.
  </Accordion>

  <Accordion title="404: Billing contact not found">
    The billing contact ID doesn't exist in your marketplace. IDs are case-sensitive and must match the ID used in Step 1 exactly. Create the contact before linking or assigning it.
  </Accordion>

  <Accordion title="404 when linking a contact to a vendor">
    If the billing contact exists, the vendor ID wasn't found. Use your own vendor ID, the same one you send in your catalog, not a Topsort internal ID.
  </Accordion>

  <Accordion title="404: Campaign not found">
    `campaignId` must be the Topsort campaign ID of a campaign in your marketplace.
  </Accordion>

  <Accordion title="422: Validation error">
    A required field is missing or empty. Upsert Billing Contact needs `advertiserName`, `brandName` and `invoiceTo` on every call.
  </Accordion>
</AccordionGroup>

<IntegrationsCard1A />


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.