Overview
Email notifications are one of the most important channels for user communication. Pingram makes it easy to send personalized, professional emails with advanced features like parameters, attachments, CC/BCC, and tracking.
This guide covers everything you need to know about sending emails, from basic setup to advanced features.
Step 1: Create your account and API key
- Create your Pingram account and complete the onboarding. Sign up for free.
- Copy a secret API key from the API Keys page in the dashboard.
- Choose a stable
typestring for each email flow (e.g.welcome_email,password_reset). You pass it in every send request—Pingram uses it to group logs and analytics, and creates the type automatically on first send.
Step 2: Install the SDK
In the dashboard send step, the first thing you’ll need to do is install the SDK. Select the appropriate language and follow the instructions.
Install the node package using one of the following package managers:
npm install pingramyarn add pingrampnpm add pingrampip install pingram-pythoncomposer require pingram/phpgo get github.com/pingram-io/pingram-godotnet add package PingramAdd the Pingram dependency to your pom.xml. Check Maven Central for the latest version.
<dependencies> <dependency> <groupId>io.pingram</groupId> <artifactId>pingram</artifactId> <version>0.1.0</version> </dependency></dependencies>gem install pingramOr add to your Gemfile: gem 'pingram'
Step 3: Send an Email Notification
The next part of the send step is to send the notification from your backend. Here’s how to send an email notification by passing the email content directly:
Replace the type value (e.g., welcome_email) with your own stable
identifier for that email flow, and use your API key (pingram_sk_…). You
can find it on the API Keys page in the Pingram dashboard.
import { Pingram } from 'pingram';
// Initialize (default US region)// For CA region: add region: 'ca' on new line after apiKey// For EU region: add region: 'eu' on new line after apiKeyconst pingram = new Pingram({ apiKey: 'Your apiKey'});
// Send email notificationawait pingram.email.send({ type: 'welcome_email', to: 'user@example.com', subject: 'Welcome to Acme Corp', html: '<h1>Welcome!</h1><p>Thanks for joining Acme Corp.</p>', fromName: 'Acme Team', fromAddress: 'hello@acme.com'});import asynciofrom pingram import Pingramfrom pingram.models import SendEmailRequest
async def send_email(): async with Pingram(api_key="pingram_sk_...") as client: await client.email.email_send( SendEmailRequest( type="welcome_email", to="user@example.com", subject="Welcome to Acme Corp", html="<h1>Welcome!</h1><p>Thanks for joining Acme Corp.</p>", from_name="Acme Team", from_address="hello@acme.com", ) )
# Run the async functionasyncio.run(send_email())use Pingram\Client;use Pingram\Model\SendEmailRequest;
$client = new Client('pingram_sk_...');
$body = new SendEmailRequest([ 'type' => 'welcome_email', 'to' => 'user@example.com', 'subject' => 'Welcome to Acme Corp', 'html' => '<h1>Welcome!</h1><p>Thanks for joining Acme Corp.</p>', 'fromName' => 'Acme Team', 'fromAddress' => 'hello@acme.com',]);$client->getEmail()->emailSend($body);package main
import ( "context" "log"
pingram "github.com/pingram-io/pingram-go")
func main() { client := pingram.NewClient("pingram_sk_...") // Your secret API key
body := pingram.NewSendEmailRequest( "welcome_email", "user@example.com", "Welcome to Acme Corp", "<h1>Welcome!</h1><p>Thanks for joining Acme Corp.</p>", ) body.FromName = pingram.PtrString("Acme Team") body.FromAddress = pingram.PtrString("hello@acme.com")
_, _, err := client.EmailAPI.EmailSend(context.Background()).SendEmailRequest(*body).Execute() if err != nil { log.Fatal(err) }}using Pingram;using Pingram.Model;
var client = new PingramClient("pingram_sk_...");var body = new SendEmailRequest( type: "welcome_email", to: "user@example.com", subject: "Welcome to Acme Corp", html: "<h1>Welcome!</h1><p>Thanks for joining Acme Corp.</p>"){ FromName = "Acme Team", FromAddress = "hello@acme.com"};await client.EmailApi.EmailSendAsync(body);package com.example;
import io.pingram.Pingram;import io.pingram.model.*;
public class Example { public static void main(String[] args) { Pingram pingram = new Pingram("pingram_sk_..."); // Your secret API key
SendEmailRequest body = new SendEmailRequest() .type("welcome_email") .to("user@example.com") .subject("Welcome to Acme Corp") .html("<h1>Welcome!</h1><p>Thanks for joining Acme Corp.</p>") .fromName("Acme Team") .fromAddress("hello@acme.com");
SendEmailApiResponse response = pingram.getEmail().emailSend(body); System.out.println("Tracking ID: " + response.getTrackingId()); }}require 'pingram'
client = Pingram::Client.new(api_key: 'pingram_sk_...')body = Pingram::SendEmailRequest.new( type: 'welcome_email', to: 'user@example.com', subject: 'Welcome to Acme Corp', html: '<h1>Welcome!</h1><p>Thanks for joining Acme Corp.</p>', from_name: 'Acme Team', from_address: 'hello@acme.com')client.email.email_send(body)You’re All Set!
🎉 Congrats! You’re now sending notifications!
If you’d like to send using templates configured in the Dashboard, check out our Templating guide and Backend Integration, or explore more advanced features below.
Advanced Email Features
Email with Attachments
You can send files as email attachments using either of two methods:
- URL-based attachments: Use this method when your file is already hosted online and can be reached by Pingram via a public or signed URL. Make sure the URL you provide is accessible at the time the email is sent - Pingram needs to be able to fetch the file directly from that address. This is efficient for static or previously uploaded files.
- Base64 content attachments: Use when your file is dynamically generated, privately held, or not accessible online. This lets you embed the file’s data directly in the request - no external hosting required.
Choose the approach that best fits how and where your file is stored or generated.
await pingram.email.send({ type: 'invoice_email', to: 'user@example.com', subject: 'Your Invoice INV-001', html: '<h1>Invoice</h1><p>Amount: $99.99</p>', attachments: [ { filename: 'invoice1.pdf', url: 'https://example.com/files/invoice.pdf' } ]});from pingram import Pingramfrom pingram.models import SendEmailRequest, SendEmailRequestAttachmentsInner
async with Pingram(api_key="pingram_sk_...") as client: await client.email.email_send( SendEmailRequest( type="invoice_email", to="user@example.com", subject="Your Invoice INV-001", html="<h1>Invoice</h1><p>Amount: $99.99</p>", attachments=[ SendEmailRequestAttachmentsInner( filename="invoice1.pdf", url="https://example.com/files/invoice.pdf", ), ], ) )$body = new SendEmailRequest([ 'type' => 'invoice_email', 'to' => 'user@example.com', 'subject' => 'Your Invoice INV-001', 'html' => '<h1>Invoice</h1><p>Amount: $99.99</p>', 'attachments' => [ ['filename' => 'invoice1.pdf', 'url' => 'https://example.com/files/invoice.pdf'], ],]);$client->getEmail()->emailSend($body);package main
import ( "context" "log"
pingram "github.com/pingram-io/pingram-go")
func main() { client := pingram.NewClient("pingram_sk_...")
body := pingram.NewSendEmailRequest( "invoice_email", "user@example.com", "Your Invoice INV-001", "<h1>Invoice</h1><p>Amount: $99.99</p>", ) body.Attachments = []pingram.SendEmailRequestAttachmentsInner{ *pingram.NewSendEmailRequestAttachmentsInner( "invoice1.pdf", "https://example.com/files/invoice.pdf", ), }
_, _, err := client.EmailAPI.EmailSend(context.Background()).SendEmailRequest(*body).Execute() if err != nil { log.Fatal(err) }}using Pingram;using Pingram.Model;
var client = new PingramClient("pingram_sk_...");
var body = new SendEmailRequest( type: "invoice_email", to: "user@example.com", subject: "Your Invoice INV-001", html: "<h1>Invoice</h1><p>Amount: $99.99</p>"){ Attachments = new List<SendEmailRequestAttachmentsInner> { new("invoice1.pdf", "https://example.com/files/invoice.pdf") }};await client.EmailApi.EmailSendAsync(body);import java.util.List;
SendEmailRequest body = new SendEmailRequest() .type("invoice_email") .to("user@example.com") .subject("Your Invoice INV-001") .html("<h1>Invoice</h1><p>Amount: $99.99</p>") .attachments(List.of( new SendEmailRequestAttachmentsInner() .filename("invoice1.pdf") .url("https://example.com/files/invoice.pdf") ));
SendEmailApiResponse response = pingram.getEmail().emailSend(body);require 'pingram'
client = Pingram::Client.new(api_key: 'pingram_sk_...')body = Pingram::SendEmailRequest.new( type: 'invoice_email', to: 'user@example.com', subject: 'Your Invoice INV-001', html: '<h1>Invoice</h1><p>Amount: $99.99</p>', attachments: [ { filename: 'invoice1.pdf', url: 'https://example.com/files/invoice.pdf' } ])client.email.email_send(body)Use only the raw base64 string (no data: prefix). Set contentType when it
cannot be inferred from the filename.
Attachment size limits
- Inline (
content, base64): ~4 MB raw per attachment. Available via the API and SMTP. Base64 encoding adds roughly 33% overhead, so a 4 MB file produces a total request payload of about 6 MB. Requests exceeding this limit return 413 Payload Too Large. - URL (
url): Up to 20 MB per attachment. Pingram fetches the file at send time. API only — not supported via SMTP. - Multiple attachments: Inline attachments share the same overall request payload budget.
For files larger than ~4 MB, host the file at a publicly accessible URL and use a URL attachment instead.
CC and BCC Recipients
Send copies of your email to additional recipients:
await pingram.email.send({ type: 'team_update', to: 'primary@example.com', subject: 'Team Update: Website Redesign', html: '<p>Status Update: New features are now available!</p>', ccAddresses: ['manager@example.com', 'team-lead@example.com'], bccAddresses: ['compliance@example.com']});from pingram import Pingramfrom pingram.models import SendEmailRequest
async with Pingram(api_key="pingram_sk_...") as client: await client.email.email_send( SendEmailRequest( type="team_update", to="primary@example.com", subject="Team Update: Website Redesign", html="<p>Status Update: New features are now available!</p>", cc_addresses=["manager@example.com", "team-lead@example.com"], bcc_addresses=["compliance@example.com"], ) )$body = new SendEmailRequest([ 'type' => 'team_update', 'to' => 'primary@example.com', 'subject' => 'Team Update: Website Redesign', 'html' => '<p>Status Update: New features are now available!</p>', 'ccAddresses' => ['manager@example.com', 'team-lead@example.com'], 'bccAddresses' => ['compliance@example.com'],]);$client->getEmail()->emailSend($body);package main
import ( "context" "log"
pingram "github.com/pingram-io/pingram-go")
func main() { client := pingram.NewClient("pingram_sk_...")
body := pingram.NewSendEmailRequest( "team_update", "primary@example.com", "Team Update: Website Redesign", "<p>Status Update: New features are now available!</p>", ) body.CcAddresses = []string{"manager@example.com", "team-lead@example.com"} body.BccAddresses = []string{"compliance@example.com"}
_, _, err := client.EmailAPI.EmailSend(context.Background()).SendEmailRequest(*body).Execute() if err != nil { log.Fatal(err) }}using Pingram;using Pingram.Model;
var client = new PingramClient("pingram_sk_...");
var body = new SendEmailRequest( type: "team_update", to: "primary@example.com", subject: "Team Update: Website Redesign", html: "<p>Status Update: New features are now available!</p>"){ CcAddresses = new List<string> { "manager@example.com", "team-lead@example.com" }, BccAddresses = new List<string> { "compliance@example.com" }};await client.EmailApi.EmailSendAsync(body);import java.util.List;
SendEmailRequest body = new SendEmailRequest() .type("team_update") .to("primary@example.com") .subject("Team Update: Website Redesign") .html("<p>Status Update: New features are now available!</p>") .ccAddresses(List.of("manager@example.com", "team-lead@example.com")) .bccAddresses(List.of("compliance@example.com"));
SendEmailApiResponse response = pingram.getEmail().emailSend(body);require 'pingram'
client = Pingram::Client.new(api_key: 'pingram_sk_...')body = Pingram::SendEmailRequest.new( type: 'team_update', to: 'primary@example.com', subject: 'Team Update: Website Redesign', html: '<p>Status Update: New features are now available!</p>', cc_addresses: ['manager@example.com', 'team-lead@example.com'], bcc_addresses: ['compliance@example.com'])client.email.email_send(body)Reply-To Addresses
Set custom reply-to addresses for two-way email communication:
await pingram.email.send({ type: 'support_ticket', to: 'customer@example.com', subject: 'Support Ticket TKT-456', html: '<p>Issue: Login problems</p>', replyToAddresses: ['support@yourcompany.com']});from pingram import Pingramfrom pingram.models import SendEmailRequest
async with Pingram(api_key="pingram_sk_...") as client: await client.email.email_send( SendEmailRequest( type="support_ticket", to="customer@example.com", subject="Support Ticket TKT-456", html="<p>Issue: Login problems</p>", reply_to_addresses=["support@yourcompany.com"], ) )$body = new SendEmailRequest([ 'type' => 'support_ticket', 'to' => 'customer@example.com', 'subject' => 'Support Ticket TKT-456', 'html' => '<p>Issue: Login problems</p>', 'replyToAddresses' => ['support@yourcompany.com'],]);$client->getEmail()->emailSend($body);package main
import ( "context" "log"
pingram "github.com/pingram-io/pingram-go")
func main() { client := pingram.NewClient("pingram_sk_...")
body := pingram.NewSendEmailRequest( "support_ticket", "customer@example.com", "Support Ticket TKT-456", "<p>Issue: Login problems</p>", ) body.ReplyToAddresses = []string{"support@yourcompany.com"}
_, _, err := client.EmailAPI.EmailSend(context.Background()).SendEmailRequest(*body).Execute() if err != nil { log.Fatal(err) }}using Pingram;using Pingram.Model;
var client = new PingramClient("pingram_sk_...");
var body = new SendEmailRequest( type: "support_ticket", to: "customer@example.com", subject: "Support Ticket TKT-456", html: "<p>Issue: Login problems</p>"){ ReplyToAddresses = new List<string> { "support@yourcompany.com" }};await client.EmailApi.EmailSendAsync(body);import java.util.List;
SendEmailRequest body = new SendEmailRequest() .type("support_ticket") .to("customer@example.com") .subject("Support Ticket TKT-456") .html("<p>Issue: Login problems</p>") .replyToAddresses(List.of("support@yourcompany.com"));
SendEmailApiResponse response = pingram.getEmail().emailSend(body);require 'pingram'
client = Pingram::Client.new(api_key: 'pingram_sk_...')body = Pingram::SendEmailRequest.new( type: 'support_ticket', to: 'customer@example.com', subject: 'Support Ticket TKT-456', html: '<p>Issue: Login problems</p>', reply_to_addresses: ['support@yourcompany.com'])client.email.email_send(body)Custom From Address
Set a custom from address and sender name for your emails:
await pingram.email.send({ type: 'marketing_newsletter', to: 'subscriber@example.com', subject: 'Your Company Newsletter', html: '<h1>New features are now available!</h1>', fromAddress: 'newsletter@yourcompany.com', fromName: 'Your Company Newsletter'});from pingram import Pingramfrom pingram.models import SendEmailRequest
async with Pingram(api_key="pingram_sk_...") as client: await client.email.email_send( SendEmailRequest( type="marketing_newsletter", to="subscriber@example.com", subject="Your Company Newsletter", html="<h1>New features are now available!</h1>", from_address="newsletter@yourcompany.com", from_name="Your Company Newsletter", ) )$body = new SendEmailRequest([ 'type' => 'marketing_newsletter', 'to' => 'subscriber@example.com', 'subject' => 'Your Company Newsletter', 'html' => '<h1>New features are now available!</h1>', 'fromAddress' => 'newsletter@yourcompany.com', 'fromName' => 'Your Company Newsletter',]);$client->getEmail()->emailSend($body);package main
import ( "context" "log"
pingram "github.com/pingram-io/pingram-go")
func main() { client := pingram.NewClient("pingram_sk_...")
body := pingram.NewSendEmailRequest( "marketing_newsletter", "subscriber@example.com", "Your Company Newsletter", "<h1>New features are now available!</h1>", ) body.FromAddress = pingram.PtrString("newsletter@yourcompany.com") body.FromName = pingram.PtrString("Your Company Newsletter")
_, _, err := client.EmailAPI.EmailSend(context.Background()).SendEmailRequest(*body).Execute() if err != nil { log.Fatal(err) }}using Pingram;using Pingram.Model;
var client = new PingramClient("pingram_sk_...");
var body = new SendEmailRequest( type: "marketing_newsletter", to: "subscriber@example.com", subject: "Your Company Newsletter", html: "<h1>New features are now available!</h1>"){ FromAddress = "newsletter@yourcompany.com", FromName = "Your Company Newsletter"};await client.EmailApi.EmailSendAsync(body);SendEmailRequest body = new SendEmailRequest() .type("marketing_newsletter") .to("subscriber@example.com") .subject("Your Company Newsletter") .html("<h1>New features are now available!</h1>") .fromAddress("newsletter@yourcompany.com") .fromName("Your Company Newsletter");
SendEmailApiResponse response = pingram.getEmail().emailSend(body);require 'pingram'
client = Pingram::Client.new(api_key: 'pingram_sk_...')body = Pingram::SendEmailRequest.new( type: 'marketing_newsletter', to: 'subscriber@example.com', subject: 'Your Company Newsletter', html: '<h1>New features are now available!</h1>', from_address: 'newsletter@yourcompany.com', from_name: 'Your Company Newsletter')client.email.email_send(body)To use a custom fromAddress from your domain, you’ll need to verify your domain first. See your Pingram dashboard under Settings -> Domain Verification for setup instructions. We strongly recommend you to verify your domain before sending emails from production.
Important Email Features
Pingram provides comprehensive email functionality with advanced features:
- High delivery through SPF, DKIM and DMARC - see your dashboard for domain verification setup
- Compliant with Google’s and Yahoo’s email sender policies and best practices for high deliverability
- Merge tags (injecting dynamic values into the email content) - see docs
- Pre-built Unsubscribe Link and Web Page - see docs. To capture unsubscribe events in your backend, see Events Webhook.
- Reply-to addresses for two-way email communication via
replyToAddresses- see docs - File attachments via
attachments- see docs - CC and BCC recipients via
ccAddressesandbccAddresses- see docs
You DON’T need another 3rd-party email service like SendGrid or SES. Through our partnerships, We allocate and manage any required email infrastructure, even dedicated IPs, for you.
Google and Yahoo Bulk Sender Requirements
Pingram automatically handles compliance with Google and Yahoo bulk sender requirements (read more on our blog):
- Authentication: SPF, DKIM, DMARC configured automatically
- Unsubscribe: One-click unsubscribe headers and links included per RFC 2369 and RFC 8058
- Compliance: All requirements handled automatically in your account setup
In light of recent announcements by Google and Yahoo, it has become mandatory for bulk email senders to implement various requirements in place to reduce spam. Our account setup process ensures that you are compliant with these requirements by configuring SPF, DKIM, DMARC correctly.
Moreover, it’s essential to ensure that your emails support an easy, one-click unsubscribe process for end-users. This includes incorporating a clearly visible unsubscribe link within the body of your messages and providing unsubscribe end-points in the email header. Pingram fully supports these functionalities, adhering to the standards set forth by RFC 2369 and RFC 8058. Our system automatically includes the necessary header and an unsubscribe footer in your outgoing messages, providing your recipients with a straightforward method to opt-out of future communications.
Tracking and Analytics
Pingram automatically tracks:
- Delivery: Email successfully delivered to mail server
- Bounces: Failed delivery attempts
- Complaints: Users marking emails as spam
- Opens: Users opening the email
- Clicks: Users clicking on links in the email
Pingram uses a custom domain for all tracking links included in your emails. This means that any tracking links in your email are instrumented with a custom domain, allowing us to accurately track when your email is opened and when a recipient clicks on a link.
View all analytics in your dashboard under Logs and Insights.
Events Webhook
Following are the events that you can receive via Events Webhook:
- Deliveries
- Failures
- Opens
- Clicks
- Unsubscribes
- Inbound replies
If you want to set up a webhook for these events (such as opens and clicks), see our Events Webhook documentation for setup instructions.
You can also receive an event when a user unsubscribes from an email via our Events Webhook (EMAIL_UNSUBSCRIBE).
Email Replies (Inbound Webhooks)
Pingram can capture replies to your email notifications and forward them to your application via webhooks. This enables use cases like:
- Creating support tickets from email replies
- Collecting user feedback
- Building conversational email flows
How it works:
- When you send an email, Pingram sets the reply-to address to a special address (e.g.,
reply-{trackingId}@replies.pingram.io) - When a recipient replies, Pingram processes the email
- The reply is forwarded to your configured webhook endpoint
- All replies are also visible in your Logs dashboard
Setup: Enable the EMAIL_INBOUND event in Webhook page. See our Inbound Messages documentation for full details and webhook payload format.
Your Own Email Team
Imagine having your own email team:
Our team monitors bounces and complaints and will reach out to you directly if we detect an alarming rate of such emails. You can also reach out to us any time to ask about best practices, to review your email content, or to help you troubleshoot a delivery issue.
For paying customers, we help transfer your existing email templates into our editor, and we can also help you build new ones.
Frequently Asked Questions
Do I need to configure SMTP to send emails?
No! Pingram handles all SMTP configuration and email infrastructure for you automatically. You don’t need to:
- Set up or configure SMTP servers
- Manage SMTP credentials
- Use third-party email services like SendGrid, Amazon SES, or Mailgun
- Deal with SMTP ports, authentication, or connection settings
Simply use our API to send emails, and we’ll handle all the underlying SMTP infrastructure, authentication (SPF, DKIM, DMARC), and delivery. For production use, you only need to verify your domain through DNS records - see Domain Verification for details.
Can I send emails without verifying my domain?
Yes! For testing and development, you can send emails immediately without any domain verification. Emails will be sent from our domain @pingram.io with no configuration required.
For production use, we strongly recommend verifying your domain so that:
- Emails come from your own domain (e.g.,
hello@yourcompany.com) - You have better deliverability and inbox placement
- Your brand is represented in the sender address
- You comply with email authentication best practices (SPF, DKIM, DMARC)
To verify your domain, go to Dashboard → Settings → Domain Verification and follow the step-by-step instructions to add DNS records. See our Domain Verification guide for detailed help.
Does NotificationAPI support using custom SMTP?
Not currently. SMTP support is on our product roadmap. If this is important for your use case, please reach out via our live chat or Slack to discuss timelines and when this could be integrated.
Need more help?
If you need help, reach out through our live chat or Slack community.