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:
- You build your newsletter in SendGrid as usual, with an empty marked spot for the ad.
- When it's ready, instead of clicking Send in SendGrid, you double-click the Ad Inserter on your computer.
- The AdButler Ad Inserter asks AdButler which ad to show, puts the ad in the marked spot, and tells SendGrid to send.
- 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.
- Go to Ad Items > Native Templates and click Add Template.
- Choose Custom Rendering / API.
- Add three variables exactly like this:
| Name | Placeholder | Type |
|---|---|---|
| Title | [%Title%] |
Text |
| Body | [%Body%] |
Text |
| Image | [%Image%] |
Image URL |
- 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>
1.2 Create your native ads
- Go to Advertisers, open the advertiser, open (or create) the campaign, and click Add Ad Item.
- Choose Native (Custom Rendering / API) and pick the template from step 1.1.
- 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).
- Click Save Ad Item. Repeat for each ad.
1.3 Create a dynamic zone for the newsletter
- Go to Publishers, open your publisher, and click Add Zone.
- Choose Standard Zone, give it a name like Newsletter Sponsor, and set its size to Dynamic.
- 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.
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)
- In SendGrid, go to Settings > API Keys and click Create API Key.
- Give it a name like AdButler Ad Inserter.
- Choose Restricted Access, and set Marketing to Full Access.
- Click Create & View and copy the key. SendGrid only shows it once. It goes into
settings.txtin 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.
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.
- 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
- 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 | — |
- 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=
Step 4: Every time you send a newsletter
- In the Ad Inserter folder, double-click:
- Windows:
Insert AdButler Ad (Windows).bat - Mac:
Insert AdButler Ad (Mac).command
- Windows:
- A window opens and lists your draft newsletters. Type the number of the one you want and press Enter.
- It tells you which ad AdButler picked.
- 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.
- 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".
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
KEYWORDSvalue. - 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_paramskeys 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_paramsis an empty array; treat that as no ad. output_json_native_template_mode:1is markup only (the default),2is variables only,3is both.output_json_template_params=1is an older alias for2.kw=a,badds keyword targeting.size=0x0is used because the zone is dynamic, so ad items of any size are eligible.
Tracking links
{ "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 |
;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
python3 python/adbutler_sponsor.py fetch(oradbutler/serve-request.sh) returns Title, Body, Image, and both links.- Path A:
sendgrid/create-test-template.sh, then send to a recipients file containing only yourself. - 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-sendwith--schedule now). - Open the email twice and click once. AdButler should show 2 impressions and 1 click (3 impressions with AccuPixel off).
- Pause the campaign and send again. The email arrives without the block, and no error is shown.
Related articles
- Serving native ads in Brightspot emails
- How to serve ads in an email newsletter
- Server side ad requests (via JSON API)
- Where to get tracking pixel links
- How to track statistics with AccuPixel
- Native ad template overview
- How to create a native ad item
- Zone overview
- AdButler REST API reference
- SendGrid: Send an email with Dynamic Templates
- SendGrid: Single Sends API