Guide
From sign-up to your first email.
Ops-Free sends email through your own Amazon SES account. Setup is connecting that account once; after that you call a single API. About 15 minutes, plus waiting for DNS if you use a domain.
I'm new to this
Never touched AWS or SES. Start here: every click is spelled out.
I already use Amazon SES
You have an AWS account and maybe verified identities. Skip ahead; here's what carries over.
I just need the API
Setup is done, or you'll do it from the dashboard. Show me the request and the errors.
Before you start
What you need
- An AWS account. Free to open at aws.amazon.com (AWS asks for a card). You pay AWS directly for the emails you send; Ops-Free doesn't sit in the middle of that bill.
- Somewhere to send from. Either a domain you can edit DNS for (best for inbox placement), or just your email address if you don't have one yet.
- About 15 minutes. DNS changes can take longer, but you can do the rest while you wait.
How it works: your app calls the Ops-Free API, we queue and send the message through your SES account, and we track what happens next. With one-click connect no secret is stored: we use a role in your account for short-lived sessions, and you can cut off access any time by deleting its CloudFormation stack in AWS. (Connecting with an access key instead stores the key encrypted.)
Walkthrough
Set up, step by step
The dashboard shows the same steps in the same order, with forms in place. This page explains what each one is for and what to do when it doesn't go as expected.
- 1
Create your Ops-Free account
Takes a minute. This is separate from your AWS account.
- Go to Sign up and enter your email and a password of at least 8 characters.
- Open the confirmation email if you're asked to, then sign in.
You'll know it worked when you land on a page titled “Let's get you sending”.
- 2
Connect your AWS account
Ops-Free needs permission to send through your SES account. One click creates a dedicated, limited role for it: no AWS login, no access key.
Option A: one-click connect (recommended)
- In the dashboard, open AWS (or the first setup step). Choose the region where your SES account lives (for example
ap-south-1) and click Connect AWS. - A new tab opens on AWS's Create stack page, already filled in. Sign in to your AWS account if asked.
- Scroll to the bottom, tick the box acknowledging that AWS will create an IAM role, and click Create stack.
- Go back to Ops-Free. It says “Waiting for AWS” and turns to Connected by itself, usually within a minute. You can close the AWS tab.
The stack creates one IAM role in your account. Only Ops-Free can use it, only with a random ID made for your connection, and only for short sessions of 15 minutes. Its permissions are the same ones listed under Option B. You can read the template before you create it. To cut off access, delete the stack in AWS (CloudFormation → the stack named
OpsFreeSES-…→ Delete).Option B: an IAM access key (advanced)
Use this if you can't create CloudFormation stacks, or prefer a key. It's under “Advanced” in the dashboard.
- In the AWS console, open IAM → Users → Create user. Name it
ops-free. Leave “console access” off and click through to create it. - Open the new user → Add permissions → Create inline policy → JSON. Paste this policy and save it:
IAM policy (JSON){ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ses:GetAccount", "ses:SendEmail", "ses:CreateEmailIdentity", "ses:GetEmailIdentity", "ses:CreateConfigurationSet", "ses:CreateConfigurationSetEventDestination", "ses:UpdateConfigurationSetEventDestination", "ses:PutEmailIdentityConfigurationSetAttributes", "sns:CreateTopic", "sns:Subscribe" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "route53:ListHostedZonesByName", "route53:ChangeResourceRecordSets" ], "Resource": "*" } ] }It allows sending, identity checks, event setup and (optionally) DNS publishing in Route 53. Nothing else in your account.
- Still on the user, open Security credentials → Create access key and choose “Application running outside AWS”. Copy the Access key ID and the Secret access key. AWS shows the secret once.
- Back in Ops-Free, paste both into the connect form with your AWS region. Use the region where your SES account lives; domains and the sandbox are per region.
You'll know it worked when the step turns green and shows “Connected” with your region. It tells you if your account is still in the SES sandbox.
Not working? See stuck on “Waiting for AWS”, access denied and which region to use.
- In the dashboard, open AWS (or the first setup step). Choose the region where your SES account lives (for example
- 3
Verify where your emails come from
Amazon only sends from addresses it knows are yours. Pick one option.
Option A: a domain (recommended)
- Type your domain, like
yourapp.com, and submit. Ops-Free creates it in your SES account and shows three CNAME records. - Add those three records where your DNS is managed (GoDaddy, Namecheap, Cloudflare, Route 53…). Use the copy buttons. Many registrars want only the part before your domain in the Name field.
- Wait. The page checks on its own, usually within minutes; DNS can take up to 72 hours.
If your DNS is in the same AWS account (Route 53), the records are added for you. Optionally, add an SPF record that includes
amazonses.com. If you already have one, edit it rather than adding a second.Option B: just an email address
- Choose “Don't have a domain?”, enter your address and submit.
- Amazon emails you a confirmation link. Click it, then press “I've clicked the link”.
You can send as that address straight away, including after you go live. You can add a domain later for better inbox placement.
You'll know it worked when it shows “verified” in green.
Stuck on pending? DNS troubleshooting.
- Type your domain, like
- 4
Create an API key
The key is what your code uses to send. You can create it while DNS is still propagating.
- Click Create API key.
- Copy it now. It's shown once and can't be recovered. Store it as a secret in your app (for example an environment variable named
OPSFREE_API_KEY), never in code.
You'll know it worked when you see a key starting with
sk_live_and a ready-to-run example request. - 5
Send a test email
Goes through the real pipeline: queue, your SES account, outcome tracking.
- Use the test form on the setup page, or run the example request from step 4.
- In the SES sandbox you can only send to verified addresses. To test without that limit, send to
success@simulator.amazonses.com.
You'll know it worked when the email appears in Activity as
sent. That's the setup complete; the page becomes your dashboard. - 6
Track deliveries and bouncesAutomatic
Set up for you when you connect AWS. It's what turns “sent” into delivered, bounced or complained, and blocks bad addresses automatically.
- Nothing to do: this runs as soon as your AWS account connects.
- If the dashboard still shows Connect events, click it. If AWS refuses, it tells you which permission is missing.
In your AWS account this creates an SNS topic named
ops-free-ses-eventsand an SES configuration set namedops-free, and subscribes Ops-Free to it. You can delete both in AWS at any time. Pressing the button again repairs a half-finished setup.You'll know it worked when a test email to
success@simulator.amazonses.commoves fromsenttodeliveredwithin a few seconds. - 7
Start sending to anyoneBefore going live
New AWS accounts are in the SES sandbox: only verified recipients, 200 emails a day. Production access lifts that.
- Open Start sending to anyone in the dashboard.
- Answer the few questions. Ops-Free writes the request to AWS for you.
- Submit it in the AWS console as shown. AWS usually replies within about a day.
You'll know it worked when the dashboard no longer shows the “SES sandbox” label on your connection.
For existing SES users
Already use Amazon SES?
You can skip most of the walkthrough. Your SES account, quotas and verified identities stay exactly as they are, and anything that sends to SES directly keeps working. Ops-Free just adds an API, queueing, retries, a blocked list and outcome tracking on top.
- Create or reuse an IAM user. A dedicated user is safer. If you reuse one, make sure it can do the following:
Sending
- ses:GetAccount
- ses:SendEmail
- ses:CreateEmailIdentity
- ses:GetEmailIdentity
Event tracking
- ses:CreateConfigurationSet
- ses:CreateConfigurationSetEventDestination
- ses:UpdateConfigurationSetEventDestination
- ses:PutEmailIdentityConfigurationSetAttributes
- sns:CreateTopic
- sns:Subscribe
Route 53 (optional)
- route53:ListHostedZonesByName
- route53:ChangeResourceRecordSets
- Connect it with the region where your identities live (step 2 above).
- Add your domain. If it already exists in that region, Ops-Free adopts it instead of failing and reads its current status, so a verified domain shows verified straight away. You won't need to touch DNS again.
- Create an API key and switch your calls to the Ops-Free API.
- Optionally connect events. Read the note below first.
Two things to know before connecting events. It sets the ops-free configuration set as the default on the domains you add here, replacing any default you had on that identity. And only emails sent through Ops-Free appear in Activity; mail you send straight through SES isn't tracked here.
If your account is already out of the sandbox, there's nothing more to do. If it isn't, see step 7.
For developers
Send from your code
One endpoint, POST https://www.opsfree.in/api/v1/send, authenticated with your API key as a bearer token. It validates the request, checks your blocked list, stores the message and queues it, then answers 202 before delivery finishes. Track the outcome in Activity or from the id it returns.
curl -X POST https://www.opsfree.in/api/v1/send \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-asha-001" \
-d '{"from":"hello@yourdomain.com","to":["customer@example.com"],"subject":"Welcome aboard","html":"<h1>Hello {{name}}</h1><p>Thanks for signing up.</p>","variables":{"name":"Asha"}}'const res = await fetch("https://www.opsfree.in/api/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPSFREE_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "welcome-asha-001",
},
body: JSON.stringify({
"from": "hello@yourdomain.com",
"to": [
"customer@example.com"
],
"subject": "Welcome aboard",
"html": "<h1>Hello {{name}}</h1><p>Thanks for signing up.</p>",
"variables": {
"name": "Asha"
}
}),
});
// 409 means this Idempotency-Key was already used: the first call went through.
if (res.status !== 202 && res.status !== 409) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message} (request ${error.requestId})`);
}
const { id } = await res.json(); // keep this to look the email up laterimport os, requests
res = requests.post(
"https://www.opsfree.in/api/v1/send",
headers={
"Authorization": f"Bearer {os.environ['OPSFREE_API_KEY']}",
"Idempotency-Key": "welcome-asha-001",
},
json={
"from": "hello@yourdomain.com",
"to": ["customer@example.com"],
"subject": "Welcome aboard",
"html": "<h1>Hello {{name}}</h1><p>Thanks for signing up.</p>",
"variables": {"name": "Asha"},
},
)
if res.status_code != 202:
err = res.json()["error"]
raise RuntimeError(f"{err['code']}: {err['message']} (request {err['requestId']})")
email_id = res.json()["id"]Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| from | string | Yes | A verified domain or email address in your connected AWS account. |
| to | string[] | Yes | 1 to 50 recipients. Duplicates are removed. |
| subject | string | With html/text | Not allowed together with templateId. |
| html | string | html or text | Not allowed together with templateId. |
| text | string | html or text | Plain-text version. Send both for best results. |
| templateId | uuid | Instead of the above | A template from your dashboard. It supplies the subject, html and text. |
| variables | object | No | Fills {{name}} placeholders in inline content or the template. |
| tags | object | No | String key/value labels for your own bookkeeping. |
Success: 202
{
"id": "3f2a1c9e-8d44-4a6b-9f10-2b7c5e0d1a33",
"status": "queued",
"message": "Accepted for asynchronous delivery"
}Failure: any other status
{
"error": {
"code": "DOMAIN_NOT_VERIFIED",
"message": "...",
"requestId": "req_..."
}
}Retrying safely. Add an Idempotency-Key header (any string up to 255 characters, unique per email you intend to send). If a request times out and you retry with the same key, you get a 409 carrying the original email's id instead of a second email. Treat that as success.
Limits. Up to 50 recipients per request. By default each key may send about 50 requests a second (bursts to 100); beyond that you get 429 RATE_LIMITED. Your SES account has its own limits too, and the sandbox is far lower.
Every response carries an X-Request-Id header. Quote it if you contact support.
Reference
What happened to an email
| Status | Meaning |
|---|---|
| queued | Accepted and stored. Waiting for a worker. |
| sending | A worker is handing it to Amazon SES. |
| sent | Amazon SES accepted it. Final outcome not known yet. |
| delivered | The recipient's mail server accepted it. Needs events connected. |
| bounced | The address doesn't exist or refused it. It's added to your blocked list. Needs events connected. |
| complained | The recipient marked it as spam. It's added to your blocked list. Needs events connected. |
| failed | It couldn't be sent, after retries. Open it in Activity to see why. |
| suppressed | Skipped on purpose: the address was already on your blocked list. |
Reference
API errors
Errors are JSON with a stable code. Branch on the code, not the message text.
| Code | HTTP | What it means and what to do |
|---|---|---|
| INVALID_REQUEST | 400 | The body is malformed or a field is missing or wrong.Read the message: it names the field. Unknown fields are rejected. A 409 with this code means the Idempotency-Key was already used: the details hold the original email's id and status, and nothing was sent twice. |
| UNAUTHORIZED | 401 | The API key is missing, wrong or revoked.Send Authorization: Bearer sk_live_… Create a new key in the dashboard if it was lost. |
| DOMAIN_NOT_VERIFIED | 403 | The from address isn't a verified domain or email address on your account.Verify it under Domains, or send from an address you already verified. |
| RECIPIENT_SUPPRESSED | 403 | At least one recipient is on your blocked list (an earlier bounce or complaint). The whole request is refused and nothing is sent.Check Blocked addresses. Remove an address only if you're sure it's valid, or drop it from to and resend. |
| RATE_LIMITED | 429 | You're sending faster than the per-key limit.Slow down and retry with backoff. Reuse the same Idempotency-Key when you retry. |
| SES_THROTTLED | 429 | AWS said you exceeded your SES sending rate.Retry with backoff. In the sandbox the limit is 1 email a second. |
| SES_REJECTED | 502 | AWS refused the message.The message says why: usually an unverified sender or a sandbox recipient restriction. |
| QUEUE_UNAVAILABLE | 503 | We couldn't queue the message. Nothing was sent.Safe to retry, ideally with the same Idempotency-Key. |
| INTERNAL_ERROR | 500 | Something broke on our side.Retry once, then contact us with the requestId from the response. |
Help
Troubleshooting
My domain stays pending after I added the DNS records
DNS changes usually show up within minutes but can take up to 72 hours. The page re-checks on its own; “Check now” forces it.
Look at the name you pasted. Many registrars (GoDaddy, Namecheap) want only the part before your domain: for abc._domainkey.yourdomain.com enter abc._domainkey. Pasting the full name makes it abc._domainkey.yourdomain.com.yourdomain.com, which never resolves.
Each record's type must be CNAME and the value must be copied exactly, including the dots. Cloudflare users: turn the proxy (orange cloud) off for these records.
Check the region. The domain is created in the region of the AWS connection. If you changed regions after adding it, add the domain again.
SPF shows “fail” but my domain is verified
Sending still works. Mail passes DMARC through DKIM, which is what verification proves. SPF is a belt-and-braces check.
To fix it, add include:amazonses.com to your existing SPF record. If you have v=spf1 include:_spf.google.com ~all, make it v=spf1 include:_spf.google.com include:amazonses.com ~all.
Never create a second SPF record. A domain with two SPF records fails SPF for everyone, including your other mail.
Connect AWS says “Waiting for AWS” and never finishes
Check the tab AWS opened. The stack must reach CREATE_COMPLETE; if you didn't tick the acknowledgement box at the bottom of the page and click Create stack, nothing is created.
In AWS open CloudFormation (in the region shown in the page's address) and look at the stack named OpsFreeSES-… If it says ROLLBACK or CREATE_FAILED, open its Events tab: the first red line says why. Delete the failed stack and click Connect AWS again.
If the stack finished but Ops-Free still waits, wait a minute: a brand-new role can take a few seconds to become usable. If it then says AWS would not let Ops-Free use the role, delete the stack and start again, or use an access key under Advanced.
An attempt expires after two hours. Starting again is always safe.
“Connect events” or domain setup says access denied
The connection is missing a permission. The error names it. If you connected with one click, click Connect AWS again (it replaces the old role with one that has the current permissions). If you connected with an access key, replace the IAM user's policy with the one in step 2 (it includes SNS and configuration-set permissions) and try again.
Every step is safe to repeat. Pressing the button again finishes a half-done setup.
I can only send to a few addresses, or it says the recipient isn't verified
New AWS accounts start in the SES sandbox: you can send only to verified addresses, up to 200 emails a day at 1 a second.
For testing, send to success@simulator.amazonses.com (delivered), bounce@simulator.amazonses.com or complaint@simulator.amazonses.com. These work in the sandbox and show up in Activity.
To send to anyone, open Start sending to anyone in the dashboard. It prepares the request for AWS, which usually reviews it within about a day.
Emails stay on “sent” and never become “delivered”
Delivery, bounce and complaint outcomes come from Amazon SNS, which is normally set up when you connect AWS. If the dashboard still shows Connect events, click it (or see step 6 below).
Emails sent before events were connected stay on “sent”. Only new emails get an outcome.
I lost my API key
Keys are shown once and stored only as a hash, so they can't be recovered. Create a new key under API keys, update your app, then revoke the old one.
Which AWS region should I use?
The region where your SES account is set up, usually the one closest to your users. SES identities and the sandbox are per region, so verify your domain in the same region you connect.
Ready?
Create an account and the dashboard walks you through these same steps.
Create an account