Targeted Ads in SendGrid

Give every subscriber their own native ad in the newsletters you send with SendGrid. Each ad is a headline, description, button, and image, styled like the rest of your newsletter. AdButler picks the ad separately for each subscriber, using what you already know about them in SendGrid (their interests, industry, role, and so on), and counts every open and click in your normal AdButler reports.

This guide is in two halves. The first half is step by step, for everyone: what to do in AdButler, what to do in SendGrid, what to add to your newsletter, and when the sync runs. The second half, For developers, has the technical details for whoever sets up and schedules the sync.

If every subscriber can see the same ad, use Native ads in SendGrid instead. It's simpler.

How it works, in 30 seconds

SendGrid can fill in details for each subscriber when it sends (like {{first_name}}), but it can't ask AdButler for an ad. So a small program, the sync, does that ahead of time:

  1. Shortly before each newsletter goes out, the sync runs.
  2. For each subscriber, it asks AdButler: "which ad should this person see?" It sends that person's details, for example industry = fintech, interests = venture. AdButler picks the ad using your normal campaign targeting.
  3. The sync saves each subscriber's ad onto their SendGrid contact, in seven fields: headline, description, button text, image, links, and tracking pixel.
  4. SendGrid sends the newsletter. Your newsletter contains placeholders such as {{scc_title}}, and SendGrid swaps in each subscriber's own ad.
  5. AdButler counts each open and each click, like any other ad.
Step Where Who How often
Set up the template, ads, zone, and targeting AdButler You Once, then per campaign
Create an API key and find your list ID SendGrid You Once
Add the ad block to the newsletter Your newsletter template You or your developer Once
Set up the sync A server Your IT team or developer Once
The sync runs Automatically, on a schedule Nobody Before every send
The sync must finish before each newsletter is sent. Once it's scheduled, it runs by itself. The only thing to keep in mind is that it has to run before every send, including one-off sends at unusual times.

Before you start

You'll need:

  • An AdButler account with native ads.
  • SendGrid Marketing Campaigns, with your subscribers stored as contacts in SendGrid.
  • The details you want to target by, stored as contact fields in SendGrid, such as industry, role, or interests.
  • Someone who can set up a scheduled job on a server, such as your IT team or a developer. This is a one-time setup.

Step 1: What you do in AdButler

1.1 Create a native template

A native template tells AdButler which pieces each ad has. This one has four.

  1. Go to Ad Items > Native Templates and click Add Template.
  2. Choose Custom Rendering / API.
  3. Add these four variables exactly as shown:
Name Placeholder Type
Title [%Title%] Text
Body [%Body%] Text
CTAText [%CTAText%] Text
Image [%Image%] Image URL
  1. In the template HTML box, paste the following, then click Save:
<div class="sponsored"><a href="[TRACKING_LINK]"><img src="[%Image%]" alt="[%Title%]" /></a><h3>[%Title%]</h3><p>[%Body%]</p><a href="[TRACKING_LINK]">[%CTAText%]</a></div>
This HTML isn't what appears in the email; your newsletter controls the look. AdButler just needs all four placeholders to appear in the template.

1.2 Create your native ads

  1. Go to Advertisers, open the advertiser and the campaign, and click Add Ad Item.
  2. Choose Native (Custom Rendering / API) and pick the template from step 1.1.
  3. Fill in the Title (headline), Body (description), CTAText (button text, such as Register now), choose or upload the Image, and enter the Destination URL.
  4. Click Save Ad Item.

1.3 Create a dynamic zone

  1. Go to Publishers, open your publisher, and click Add Zone.
  2. Choose Standard Zone, name it something like Newsletter Sponsor, and set its size to Dynamic.
  3. Save it and write down the zone ID.
Only put native ads from step 1.2 in this zone. Image or HTML ads can't appear in this kind of newsletter ad.

1.4 Target each campaign

This is how AdButler decides who sees what. Assign each campaign to the zone, and add targeting to the placement:

  • Keyword targeting: for example, the Fintech Summit campaign targets the keyword fintech. Anyone whose interests field includes fintech can get it.
  • Data key targeting: for example, target industry = Banking or role = Analyst.

Leave one campaign untargeted as a fallback, so subscribers who don't match anything still get an ad. Weights, dates, and limits work as usual.

1.5 Turn on AccuPixel

Go to Settings > Ad Serving and turn on AccuPixel. This makes sure the sync's own requests aren't counted as impressions. Only real opens are.

1.6 Create an AdButler API key

Go to Settings > API Settings and create an API key. Your IT team needs it, along with your account ID and the zone ID from step 1.3.

Step 2: What you do in SendGrid

2.1 Create a SendGrid API key

  1. Go to Settings > API Keys and click Create API Key.
  2. Name it AdButler sync, choose Restricted Access, and set Marketing to Full Access.
  3. Click Create & View and copy the key. It's only shown once. Give it to your IT team securely.

2.2 Find your newsletter's audience

  1. Go to Marketing > Contacts.
  2. Open the list (or segment) your newsletter is sent to.
  3. Copy its ID: the long code at the end of the page address in your browser.

2.3 Note your targeting fields

Go to Marketing > Custom Fields and note the names of the fields that hold your subscribers' details, for example industry, role, and interests. Decide which ones match your AdButler targeting:

SendGrid field AdButler targeting
interests (e.g. fintech, venture) Keywords
industry (e.g. Banking) Data key industry
role (e.g. Analyst) Data key role

You don't need to create the seven ad fields; the sync creates them the first time it runs. They all start with scc_.

Step 3: Add the ad to your newsletter

Wherever your newsletter's HTML lives (in your CMS, such as Brightspot, or in SendGrid), add the ad block once where the ad should appear. Copy it from sendgrid/contact-fields-sponsor-block.html in the kit. In short, it's:

{{#if scc_title}}
  <a href="{{{scc_clickurl}}}"><img src="{{{scc_banner}}}" alt="{{scc_title}}" width="600"></a>
  <h3>{{scc_title}}</h3>
  <p>{{scc_description}}</p>
  <a href="{{{scc_ctalink}}}">{{scc_ctatext}}</a>
  <img src="{{{scc_impressionurl}}}" width="1" height="1" alt="">
{{/if}}
Placeholder What it shows
{{scc_title}} The headline
{{scc_description}} The description
{{scc_ctatext}} The button text
{{{scc_banner}}} The image
{{{scc_clickurl}}}, {{{scc_ctalink}}} The click link. Both are the same AdButler link, so every click is counted.
{{{scc_impressionurl}}} An invisible pixel that counts each open
  • Change the look freely: the fonts, colours, button style, and image size are yours. Keep the placeholders as they are.
  • Leave the {{#if scc_title}} wrapper in place: it hides the whole block for anyone who has no ad.
Copy the placeholders exactly, including the triple braces {{{ }}} around the links and the image. Triple braces stop SendGrid from altering the web addresses.

Step 4: Set up the sync (one time, by your IT team)

Send your IT team or developer this article and the kit, adbutler-sendgrid-developer-kit.zip, along with:

What From
AdButler account ID, zone ID, and API key Step 1
SendGrid API key and list or segment ID Step 2
Which fields are keywords and which are data keys Step 2.3
When each newsletter is sent You

They'll install it on a server, do a test run that shows which ad each subscriber would get without changing anything, and schedule it to run before each send. All the details are in For developers.

Step 5: Every time a newsletter goes out

Once it's set up, you don't need to do anything. Just keep these in mind:

  • The sync runs on a schedule, for example 60 minutes before each newsletter. Give it enough time to finish; for very large lists, your IT team will tell you how long a run takes.
  • Sending at an unusual time? Ask IT to run the sync first. Otherwise subscribers see the ads from the previous send.
  • Changing the ads? Update your campaigns in AdButler as usual. The next sync picks up the changes.

What happens after you send

  • Each subscriber sees their own ad, chosen for them at the time of the sync.
  • Someone opens the email: AdButler counts one impression, every time it's opened.
  • Someone clicks the ad or the button: AdButler counts one click, then sends them to the advertiser's site.
  • Subscribers with no matching ad see no ad block at all.

If something goes wrong

What you notice What to check
Nobody sees an ad Did the sync run before this send? Is the ad block (step 3) in the newsletter?
Subscribers see last time's ads The sync didn't run before this send. Run it before every send.
Nobody in a group sees the right ad Check that the campaign's targeting (step 1.4) matches the field values in SendGrid exactly, including spelling.
Many subscribers have no ad Add an untargeted fallback campaign (step 1.4).
Text shows {{scc_title}} literally The placeholders were changed or removed by the newsletter tool. Ask your developer to check that they reach SendGrid unchanged.

Good to know

  • Geography doesn't work for this kind of ad. The ads are chosen on a server, not in the subscriber's location, so target by your subscribers' data rather than by location.
  • Cookie-based frequency caps don't apply for the same reason.
  • Each subscriber uses one ad request per send from your AdButler plan, plus one per open.
  • Apple Mail pre-loads images, so some opens are counted automatically, as with every email platform.
  • SendGrid may wrap links with its own click tracking. Clicks still go through AdButler.

For developers

Everything below is for whoever installs and schedules the sync.

What the sync does

node/sync-contacts.mjs (Node 18+) and python/sync_contacts.py (Python 3.8+) are identical in behaviour, and neither has dependencies:

  1. Export the audience. POST /v3/marketing/contacts/exports with SYNC_LIST_IDS / SYNC_SEGMENT_IDS (CSV), poll GET /v3/marketing/contacts/exports/{id} until ready, and download the files. The files may be gzipped. Alternatively, --contacts-file reads a CSV, JSON array, or JSON-lines file.
  2. Decide each subscriber's ad, one request per contact: GET https://servedbyadbutler.com/adserve/;ID={ACCOUNT};size=0x0;setID={ZONE};type=json;output_json_native_template_mode=2;kw=fintech,venture;_abdk_json={"industry":"Banking","role":"Analyst"}
    • Keywords come from SYNC_KEYWORD_FIELDS; comma-separated values are split.
    • Data keys come from SYNC_DATAKEY_FIELDS. Write industry to use the field name as the key, or key:field to rename it.
    • Field lookup is case-insensitive.
  3. Get tracking links once per distinct ad, using GET /v2/manual-tracking-links?placement=&ad_item=&type=impression,click_redirect. They're cached for the run; failures aren't cached.
  4. Write the custom fields. The seven scc_* Text fields are created if missing (/v3/marketing/field_definitions). Contacts are upserted with PUT /v3/marketing/contacts in batches under 5 MB / 30,000 contacts, and the script polls GET /v3/marketing/contacts/imports/{job_id} until each job is completed. It exits non-zero on any failure.
Field Value
scc_title, scc_description, scc_ctatext, scc_banner Native variables Title, Body, CTAText, Image (names configurable)
scc_ctalink, scc_clickurl The manual click_redirect link
scc_impressionurl The manual impression pixel plus ;cb=<per-subscriber hash>, which keeps the signature valid and stops proxy caching
(no eligible ad, error, non-native winner) All seven empty, so stale ads are cleared

Install

  1. Unzip adbutler-sendgrid-developer-kit.zip on the server. Node 18+ or Python 3.8+ is required, with nothing else to install.
  2. Copy .env.example to .env and fill it in:
ADBUTLER_ACCOUNT_ID=123456
ADBUTLER_ZONE_ID=789012
ADBUTLER_API_KEY=...
SENDGRID_API_KEY=SG....
SYNC_LIST_IDS=1a2b3c4d-....            # or SYNC_SEGMENT_IDS
SYNC_KEYWORD_FIELDS=interests
SYNC_DATAKEY_FIELDS=industry,role
# ADBUTLER_BODY_VAR=Body  ADBUTLER_CTATEXT_VAR=CTAText   # if your template uses other names
  1. Test without writing anything. Example output (yours will differ):
node node/sync-contacts.mjs --dry-run --limit 20
exporting audience from SendGrid (job 9b2f...)...
20 contacts
decided in 0.6s: 20 ad requests, 18 with an ad, 2 without (0 errors, 0 non-native)
  ad item 523700001: 11
  ad item 523700002: 7
dry run, nothing written. First 3 contacts:
[ { "email": "pat@example.com", "fields": { "title": "Fintech Summit", ... } } ]
  1. Do a full run (example output below), then check a few contacts in SendGrid (Marketing > Contacts, open a contact, and look for the scc_ fields):
node node/sync-contacts.mjs
250000 contacts
decided in 212.4s: 250000 ad requests, 241002 with an ad, 8998 without (0 errors, 0 non-native)
  batch 1/84: 2980 contacts queued (job 3f1c...)
  ...
waiting for SendGrid to apply the updates...
done in 1093.0s: 250000 contacts updated. The newsletter can be sent now.

The run time grows with the list size and the network. Time a full run with your real list, and use that figure for the schedule in the next step.

Schedule it

Run the sync before each send, with room to spare beyond the time a full run takes. For example, for a newsletter at 6:00 every weekday:

# Linux cron: 4:30 every weekday
30 4 * * 1-5  cd /opt/adbutler-sendgrid && /usr/bin/node node/sync-contacts.mjs >> sync.log 2>&1
:: Windows Task Scheduler, action
Program:    C:\Program Files\nodejs\node.exe
Arguments:  node\sync-contacts.mjs
Start in:   C:\adbutler-sendgrid
  • Other schedulers: a cloud scheduler (AWS EventBridge, Azure, Google Cloud Scheduler) running a container or function works too. The process is a single run-to-completion command.
  • Alert on failure: it exits with code 1 on any failure. Hook that into your alerting, so a failed sync never goes unnoticed before a send.
  • Several newsletters: use a separate .env (or environment variables) per newsletter, if they have different audiences or zones.

Options

Flag or setting Purpose
--dry-run Decide and print, but write nothing
--limit N Only the first N contacts (for testing)
--contacts-file FILE Read contacts from a CSV/JSON file instead of exporting from SendGrid
--clear Set all seven fields to empty on every contact, without asking AdButler
SYNC_CONCURRENCY Ad requests in flight (default 25)
SYNC_GROUP_IDENTICAL=1 One ad request per distinct targeting combination instead of per subscriber. Far fewer requests, but everyone in a group gets the same ad.
SYNC_FIELD_PREFIX Field-name prefix (default scc_)
ADBUTLER_TITLE_VAR, ADBUTLER_BODY_VAR, ADBUTLER_CTATEXT_VAR, ADBUTLER_IMAGE_VAR Native template variable names
SYNC_SEND_ID Seed for the per-subscriber pixel token (default: today's date)

Scale and limits

  • Ad requests: one per subscriber per sync. They count toward the account's ad request volume. With AccuPixel on, they record no impressions; only pixel loads (opens) do.
  • Throughput: in testing, 200,000 subscribers were decided in about 9 seconds against a local mock, using about 1 GB of memory. Against the real ad servers, the time depends on network latency and SYNC_CONCURRENCY.
  • Geo targeting: won't work, because requests come from the sync server's IP address.
  • Cookie-based frequency caps: don't apply.
  • User agent: requests carry AdButler-SendGrid-Sync/1.0, which isn't treated as a bot by AdButler.
  • Not sending a stale ad: the fields persist on contacts, so every send needs a fresh sync. --clear after a send is optional.

Testing checklist

  1. Run --dry-run --limit 20 and confirm the ads match each subscriber's targeting.
  2. Do a full run against a list containing only your own test contacts, with different field values.
  3. Check the scc_ fields on those contacts in SendGrid.
  4. Send the newsletter to that list. Each test contact should receive its own ad.
  5. Open one email twice and click once. AdButler should show 2 impressions and 1 click for that ad.
  6. Pause all campaigns, sync again, and send. No ad block should appear.

Can't find what you're looking for?

Send us an email

hello@adbutler.com

Visit the blog

For more ad serving tips, industry news and AdButler insights.