Serving Native Ads in SendGrid

Put an AdButler native ad (an image, a headline, and a short description, styled like the rest of your newsletter) into the emails you send with SendGrid. AdButler picks the ad, and counts every open and every click in your normal AdButler reports.

This guide is in two halves. The first half is for everyone: what to do in AdButler, what to do in SendGrid, and how to add the ad each time you send. No coding is needed. The second half, For developers, has the technical details for teams whose own software sends email through SendGrid.

How it works, in 30 seconds

SendGrid can't fetch an ad by itself; it only sends the email you give it. So a small program, the AdButler Ad Inserter, does it for you right before you send:

  1. You build your newsletter in SendGrid as usual, with an empty marked spot for the ad.
  2. When it's ready, instead of clicking Send in SendGrid, you double-click the Ad Inserter on your computer.
  3. The AdButler Ad Inserter asks AdButler which ad to show, puts the ad in the marked spot, and tells SendGrid to send.
  4. From then on, AdButler counts each open and each click on its own. You don't need to do anything else.
Step Where Who does it How often
Set up the zone and ads AdButler You Once, and whenever you add new ads
Mark the ad spot SendGrid You Once per newsletter (or once in a template you copy)
Fill in settings.txt Your computer You Once
Double-click the Ad Inserter Your computer You Every time you send

%n Everyone who receives that newsletter sees the same ad, chosen at the moment you run the Ad Inserter. The ad does not change if someone opens the email again later.

Which setup is right for you?

How you send email with SendGrid What to use
You build newsletters on SendGrid's website (Marketing > Single Sends) The AdButler Ad Inserter. Follow Steps 1 to 4 below. Download: AdButler-SendGrid-Ad-Inserter.zip
Your own website or app sends email through SendGrid (your developer set this up) The developer kit. Do Step 1 below, then give your developer the For developers section. Download: adbutler-sendgrid-developer-kit adbutler-sendgrid-developer-kit.zip
You'd rather each person get a different ad, picked when they open the email, and image-only ads are fine An email zone. This needs no program at all. See How to serve ads in an email newsletter.

Step 1: What you do in AdButler

You only do this once. If you already serve native ads, you may have some of it already.

1.1 Create a native template

A native template tells AdButler which pieces each ad has. This one has three: a headline, a description, and an image.

  1. Go to Ad Items > Native Templates and click Add Template.
  2. Choose Custom Rendering / API.
  3. Add three variables exactly like this:
Name Placeholder Type
Title [%Title%] Text
Body [%Body%] 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><a href="[TRACKING_LINK]">[%Title%]</a></h3><p>[%Body%]</p></div>
This HTML isn't what appears in the email. The Ad Inserter uses its own email-friendly layout. AdButler just needs all three placeholders to appear in the template.

1.2 Create your native ads

  1. Go to Advertisers, open the advertiser, open (or create) 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 (up to 90 characters), the Body (up to 300 characters), choose or upload the Image, and enter the Destination URL (where a click should go).
  4. Click Save Ad Item. Repeat for each ad.

1.3 Create a dynamic zone for the newsletter

  1. Go to Publishers, open your publisher, and click Add Zone.
  2. Choose Standard Zone, give it a name like Newsletter Sponsor, and set its size to Dynamic.
  3. Save it, and write down the zone ID. You'll need it in Step 3.

Use a separate zone for each ad spot if your newsletter has more than one.

Only put native ads (from step 1.2) in this zone. If an image or HTML ad is picked, the newsletter goes out without an ad.

1.4 Put the campaign in the zone

Assign the campaign to the zone with a schedule, as you would for a website. Your usual weights, start and end dates, and limits all apply. AdButler uses them to pick the ad each time you send.

1.5 Turn on AccuPixel (recommended)

Go to Settings > Ad Serving and turn on AccuPixel. Without it, each send records one extra impression on top of the real opens.

1.6 Create an AdButler API key

Go to Settings > API Settings and create an API key. Copy it somewhere safe; it goes into settings.txt in Step 3.

Step 2: What you do in SendGrid

2.1 Create a SendGrid API key (once)

  1. In SendGrid, go to Settings > API Keys and click Create API Key.
  2. Give it a name like AdButler Ad Inserter.
  3. Choose Restricted Access, and set Marketing to Full Access.
  4. Click Create & View and copy the key. SendGrid only shows it once. It goes into settings.txt in Step 3.

2.2 Mark the ad spot in your newsletter

Build your newsletter in Marketing > Single Sends as you normally do. Where you want the ad to appear, add these two lines, exactly as written:

<!-- ADBUTLER_SPONSOR_START -->
<!-- ADBUTLER_SPONSOR_END -->
  • Code editor: paste the two lines into the HTML where the ad should go.
  • Design editor: drag a Code module to where the ad should go, and paste the two lines into it.

The lines are invisible to your readers. Leave the space between them empty; the Ad Inserter fills it in. When you're done, save the newsletter as a draft. Don't send it from SendGrid yet.

If you copy last week's newsletter to start each new one, the markers come along with it, so you only need to add them once.

Step 3: Set up the Ad Inserter (once)

The Ad Inserter runs on Windows 10 or 11, or any Mac. You don't need to install anything.

  1. Download AdButler-SendGrid-Ad-Inserter.zip and unzip it into a folder you'll remember, such as your Documents folder. AdButler Ad Inserter Download Link
  2. Open settings.txt in that folder (double-click it). Paste each value after the = sign:
Line in settings.txt What to paste Where to find it
ADBUTLER_ACCOUNT_ID= Your AdButler account number The number after ID= in any of your AdButler zone tags
ADBUTLER_ZONE_ID= The zone ID from step 1.3 Your zone's page in AdButler
ADBUTLER_API_KEY= The key from step 1.6 AdButler > Settings > API Settings
SENDGRID_API_KEY= The key from step 2.1 You copied it when you created it
KEYWORDS= Optional. Leave blank unless you target ads by keyword —
  1. Save settings.txt.

When you're done, the file looks like this, with your own values:

ADBUTLER_ACCOUNT_ID=123456
ADBUTLER_ZONE_ID=789012
ADBUTLER_API_KEY=a1b2c3d4e5f6...
SENDGRID_API_KEY=SG.xxxxxxxx...
KEYWORDS=
settings.txt contains your API keys. Keep it on your own computer, and don't email it or share it.

Step 4: Every time you send a newsletter

When to run it: right when the newsletter is ready to go, instead of clicking Send in SendGrid. The ad is picked at the moment you run the Ad Inserter.
  1. In the Ad Inserter folder, double-click:
    • Windows: Insert AdButler Ad (Windows).bat
    • Mac: Insert AdButler Ad (Mac).command
  2. A window opens and lists your draft newsletters. Type the number of the one you want and press Enter.
  3. It tells you which ad AdButler picked.
  4. Type 1 to add the ad and send the newsletter now, or type 2 to just add the ad so you can preview it in SendGrid and send it from there yourself.
  5. Press any key to close the window.

Here's what the window looks like:

  Insert AdButler Ad into a SendGrid newsletter
  ---------------------------------------------

Step 1 of 3: Finding your draft newsletters in SendGrid...

  1)  October Newsletter
  2)  Product Update Draft

Type the number of the newsletter to add the ad to, then press Enter: 1

Step 2 of 3: Asking AdButler which ad to show...
  Ad picked: "Spring sale: 30% off everything" (AdButler ad item 523700001)

Step 3 of 3: What would you like to do?
  1)  Add the ad and SEND the newsletter now
  2)  Just add the ad (I will review it and send it from SendGrid myself)
Type 1 or 2, then press Enter: 1

  Done: the ad was added to "October Newsletter".
  Sent: SendGrid status is now "scheduled".
The first time you double-click, your computer may ask whether you trust the file. This only happens once. On Windows, if you see "Windows protected your PC", click More info, then Run anyway. On a Mac, if it says the file "cannot be opened because it is from an unidentified developer", right-click (or Control-click) the file, choose Open, then click Open again.

You can run the Ad Inserter on the same draft as many times as you like. Each run swaps in a fresh ad. To change the ad on a newsletter that's already scheduled, unschedule it in SendGrid first, then run the Ad Inserter again.

What happens after you send

  • Someone opens the email: AdButler counts one impression. Every open counts, even days later.
  • Someone clicks the ad: AdButler counts one click, then sends them straight to the advertiser's site.
  • You and your advertisers see these in the usual AdButler reports, like any other ad.
  • No ad available? If no campaign is active for the zone when you run the Ad Inserter, the ad spot is left empty and the newsletter goes out without an ad. Nothing breaks.

If something goes wrong

The Ad Inserter always tells you in plain words what happened, and it never sends a newsletter halfway.

The window says What to do
settings.txt is missing: ... Open settings.txt and fill in the lines it names.
... does not have the ad markers in it yet Add the two marker lines to that newsletter (step 2.2) and save it.
SendGrid did not accept the SENDGRID_API_KEY Copy the key into settings.txt again. Check that it has Full Access to Marketing (step 2.1).
AdButler did not accept the ADBUTLER_API_KEY Copy the AdButler key into settings.txt again (step 1.6).
No draft Single Sends found Save your newsletter in SendGrid as a draft, then run it again.
No ad is available right now No AdButler campaign is active for that zone. Check the campaign's schedule and placement (step 1.4).
The ad AdButler picked is not a native ad An image or HTML ad is in the zone. Remove it, leaving only native ads (step 1.3).

Good to know

  • One ad per send. Everyone on the list sees the same ad. To show different sponsors to different audiences, make a separate newsletter per audience, and run the Ad Inserter for each with a different KEYWORDS value.
  • Opens include some automatic ones. Apple Mail pre-loads images, so some opens are counted even if the person didn't look. This affects every email platform the same way.
  • SendGrid click tracking is fine. SendGrid may wrap the ad's link with its own. The click still goes through AdButler, then to the advertiser.
  • Open counts won't exactly match SendGrid's. SendGrid and AdButler count opens separately, so the numbers will be close but not identical.
  • Each open uses one ad request from your AdButler plan.

For developers

Everything below is for developers whose own code sends email through SendGrid, or anyone who wants to know exactly what the Ad Inserter does. You don't need it for Steps 1 to 4.

What happens under the hood

Immediately before a send, two server-side requests are made to AdButler, then the result is handed to SendGrid:

# Request Returns AdButler records
1 GET https://servedbyadbutler.com/adserve/;ID={ACCOUNT_ID};size=0x0;setID={ZONE_ID};type=json;output_json_native_template_mode=2 The winning native ad's variables: Title, Body, Image, plus its banner_id and placement_id Nothing with AccuPixel on; one impression with it off
2 GET https://api.adbutler.com/v2/manual-tracking-links?placement={placement_id}&ad_item={banner_id}&type=impression,click_redirect&protocol=https with Authorization: Basic {API_KEY} A permanent impression pixel and click link for that ad Nothing
3 SendGrid API Email API: the five values go in dynamic_template_data.sponsor. Single Sends: an HTML block is written between the markers. —

After that, each open loads the pixel (one impression) and each click goes through the click link (one click), indefinitely.

Two ways to integrate

A. Email API with Dynamic Templates B. Marketing Campaigns Single Sends
You send with POST /v3/mail/send and a d-... template ID Single Sends built in the SendGrid UI
The ad goes in dynamic_template_data.sponsor, rendered by a block in your template HTML written between <!-- ADBUTLER_SPONSOR_START --> and <!-- ADBUTLER_SPONSOR_END -->
Per recipient A unique pixel URL per recipient (cache-buster) Same HTML for everyone
Kit code node/send-dynamic-template.mjs, python/adbutler_sponsor.py send The Ad Inserter, or node/refresh-single-send.mjs, python/adbutler_sponsor.py refresh

A. Email API with Dynamic Templates

1. Add the block to your template. In Email API > Dynamic Templates, edit the active version and paste sendgrid/dynamic-template-sponsor-block.html where the ad should appear. In essence:

{{#if sponsor}}
<a href="{{{sponsor.click_url}}}"><img src="{{{sponsor.image_url}}}" alt="{{sponsor.title}}" width="600"></a>
<h3><a href="{{{sponsor.click_url}}}">{{sponsor.title}}</a></h3>
<p>{{sponsor.body}}</p>
<img src="{{{sponsor.pixel_url}}}" width="1" height="1" alt="">
{{/if}}

Double braces HTML-escape the advertiser's text; triple braces are used only for the AdButler URLs. When there's no ad, sponsor is absent and the block is omitted. Style the block however you like; AdButler only supplies values.

2. Fetch the sponsor at send time and pass it to SendGrid. Call fetchSponsor() once per send, or once per keyword segment:

import { fetchSponsor } from './adbutler-sponsor.mjs';

const sponsor = await fetchSponsor({ keywords: 'sports' });   // null when no ad is eligible

await fetch('https://api.sendgrid.com/v3/mail/send', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    from: { email: 'news@example.com' },
    template_id: 'd-0123456789abcdef0123456789abcdef',
    personalizations: recipients.map((r) => ({
      to: [{ email: r.email }],
      dynamic_template_data: { ...r.data, ...(sponsor ? { sponsor } : {}) },
    })),
  }),
});
from adbutler_sponsor import fetch_sponsor
sponsor = fetch_sponsor(keywords="sports")   # dict or None

The reference script does this end to end. It reads a recipients file, makes one ad request per distinct kw, adds a per-recipient pixel cache-buster, and sends in batches of 1,000 recipients (SendGrid's limit per request):

node node/send-dynamic-template.mjs --recipients recipients.json [--dry-run]
python3 python/adbutler_sponsor.py send --recipients recipients.json [--dry-run]
[
  { "email": "reader@example.com", "name": "Reader", "data": { "first_name": "Sam" } },
  { "email": "fan@example.com", "kw": "sports" }
]

B. Single Sends from the command line

The Ad Inserter is the double-click version of this. The scripts below do the same thing, for running from a scheduler or job:

node node/refresh-single-send.mjs --single-send <id> [--kw sports] [--schedule now|2026-10-07T14:00:00Z] [--dry-run]
python3 python/adbutler_sponsor.py refresh --single-send <id> [--kw sports] [--schedule now] [--dry-run]

The script reads the draft (GET /v3/marketing/singlesends/{id}), replaces what's between the markers, and writes it back with PATCH /v3/marketing/singlesends/{id}, keeping the rest of email_config. With --schedule it then calls PUT /v3/marketing/singlesends/{id}/schedule. It refuses to edit anything that isn't a draft.

Configuration

The developer kit reads a .env file (copy .env.example), or regular environment variables. The Ad Inserter reads settings.txt and accepts the same AdButler keys.

Variable Required Notes
ADBUTLER_ACCOUNT_ID, ADBUTLER_ZONE_ID, ADBUTLER_API_KEY Yes
SENDGRID_API_KEY Yes Mail Send for path A; Marketing for path B
SENDGRID_FROM_EMAIL, SENDGRID_TEMPLATE_ID Path A
ADBUTLER_TITLE_VAR, ADBUTLER_BODY_VAR, ADBUTLER_IMAGE_VAR No If your template's variables aren't named Title, Body, and Image
ADBUTLER_TIMEOUT_MS No Default 5000

The ad response

{
  "status": "SUCCESS",                       ★  "NO_ADS" when no ad is eligible
  "placements": {
    "placement_1": {
      "banner_id": "523700001",              ★  ad_item for the tracking links request
      "campaign_id": "1401950",
      "placement_id": "3612500",             ★  placement for the tracking links request
      "body": "",
      "template_params": {                   ★
        "Title": "Spring sale: 30% off everything",
        "Body":  "Three days only. Free shipping on orders over $50.",
        "Image": "https://servedbyadbutler.com/getad.img?libBID=98765"
      },
      "redirect_url": "...",                 ✗  single use, expires: never put in email
      "accupixel_url": "...",                ✗  single use, expires: never put in email
      "image_url": "...", "viewable_url": "...", "eligible_url": "...", "beacons": []
    }
  }
}
  • template_params keys are the placeholders without [% %]. Text comes back as plain strings (escape it), image URLs as URLs, numbers as numbers, file variables as a full <img> tag.
  • If the winner isn't a native template ad, template_params is an empty array; treat that as no ad.
  • output_json_native_template_mode: 1 is markup only (the default), 2 is variables only, 3 is both. output_json_template_params=1 is an older alias for 2.
  • kw=a,b adds keyword targeting.
  • size=0x0 is used because the zone is dynamic, so ad items of any size are eligible.
{ "object": "manual_tracking_links",
  "data": {
    "impression":     "https://servedbyadbutler.com/adserve/;...type=tpixel...;hc=...",
    "click_redirect": "https://servedbyadbutler.com/redirect.spark?..." } }

These are the same links shown under Manual Tracking Links on a placement's page.

accupixel_url / redirect_url from the ad response Manual tracking links
Lifetime Short window after the request No expiry
Reuse Counted once Counted every time
Made for A live web page Email, cached creatives, native ads
Per-recipient pixel (path A): the kit appends ;cb=<token> to the impression URL, where the token is a hash rather than the recipient's address. The pixel's signature covers only the part before ;hc=, so validation is unaffected, and image proxies can't answer many recipients' opens with one cached copy of the pixel.

Error handling

fetchSponsor() and fetch_sponsor() never throw. On NO_ADS, HTTP errors, timeouts, a non-native winner, or a failed tracking-links request they return no sponsor, and the email goes out without the block. A sponsor is never returned without both tracking links. The Ad Inserter behaves the same way, and stops before changing anything if settings, keys, or markers are wrong.

What's in the downloads

AdButler-SendGrid-Ad-Inserter.zip (for Steps 1 to 4) AdButler Ad Inserter
File Purpose
Insert AdButler Ad (Windows).bat Double-click on Windows. Runs helpers/insert-ad-windows.ps1 with the built-in Windows PowerShell.
Insert AdButler Ad (Mac).command Double-click on Mac. Runs helpers/insert-ad-mac.js with the built-in osascript and curl.
settings.txt Account IDs and API keys.
READ ME FIRST.txt A one-page version of Steps 2 to 4.

adbutler-sendgrid-developer-kit.zip AdButler SendGrid Developer Kit
File Purpose
node/adbutler-sponsor.mjs fetchSponsor(), renderSponsorHtml(), withCacheBuster(). Node 18+, no dependencies.
node/send-dynamic-template.mjs, node/refresh-single-send.mjs Path A and path B reference scripts.
python/adbutler_sponsor.py The same in Python 3.8+ (standard library only), with fetch, send, and refresh commands.
sendgrid/dynamic-template-sponsor-block.html, single-send-sponsor-markers.html The template block and the markers.
sendgrid/create-test-template.sh, create-test-single-send.sh Create a test Dynamic Template and a draft Single Send.
adbutler/setup.sh, serve-request.sh, teardown.sh Create, query, and remove demo AdButler objects through the v2 API.
test/run-tests.mjs Offline tests against a mock AdButler and SendGrid.

Testing checklist

  1. python3 python/adbutler_sponsor.py fetch (or adbutler/serve-request.sh) returns Title, Body, Image, and both links.
  2. Path A: sendgrid/create-test-template.sh, then send to a recipients file containing only yourself.
  3. Path B: add the markers to a draft Single Send sent to a list containing only yourself, then run the Ad Inserter (or refresh-single-send with --schedule now).
  4. Open the email twice and click once. AdButler should show 2 impressions and 1 click (3 impressions with AccuPixel off).
  5. Pause the campaign and send again. The email arrives without the block, and no error is shown.

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.