API: Send Email
Send one transactional email — an order confirmation, a password reset, a one-time code — from your own application. Write the message yourself with html / text, or send a saved template with merge-tag variables. Every call is recorded, so you can see exactly what happened to it under API Sends.
| Method | POST |
| Endpoint | /api/v1/send |
| Credits used | Delivery + verification of the recipient (+ tracking, if you turn it on). Nothing is charged when the email is not sent |
| Requires | A paid plan with API access, and an account approved for campaign access (the same approval Campaigns need) |
Your first email
The smallest valid call is a recipient, a subject and a body. With no from_email it sends from your account's own Pingovo address.
curl -X POST https://pingovo.com/api/v1/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"subject": "Your order has shipped",
"html": "<p>Hi Alex, your order #1042 is on its way.</p>",
"tags": ["orders"]
}'Message
| Field | Type | Required | Description |
|---|---|---|---|
| to | string | Yes | The recipient email address. |
| subject | string | With html / text | Subject line. Not needed when you use templateId. |
| html | string | One of html, text or templateId | HTML body. Required for open / click tracking. |
| text | string | One of html, text or templateId | Plain-text body. |
| templateId | string | One of html, text or templateId | The id of a template in your library — your own or a Pingovo system template. |
| variables | object | No | Values for the template’s merge tags, e.g. { "firstName": "Alex" }. |
| from_email | string | No | Send as any verified Sender on your account. Omit to send from your account’s own Pingovo address. SMTP senders are not supported on this endpoint. |
| from_name | string | No | Display name shown next to the address. Characters that could break the header are removed. |
Grouping and tracking
| Field | Type | Default | Description |
|---|---|---|---|
| tags | string[] | — | Group sends into streams. Up to 5 tags, 40 characters each: letters, numbers, spaces and _ . : - |
| campaign | string | — | Shorthand for one more tag. |
| track_opens | boolean | false | Add an open-tracking pixel. Needs an html body. |
| track_clicks | boolean | false | Rewrite links so clicks are counted. Needs an html body. |
Safety and pacing
| Field | Type | Default | Description |
|---|---|---|---|
| verify_before_send | boolean | true | Verify the recipient first and never send to an address that comes back invalid. Only accounts granted a direct-send override can switch this off; for everyone else the flag is ignored and verification still runs. |
| skip_risky | boolean | false | Leave out risky, catch-all and unverifiable addresses. Needs verify_before_send. |
| skip_role_based | boolean | false | Leave out shared inboxes such as info@, sales@ and support@. Needs verify_before_send. |
| delay_between_emails | number (seconds) | none | Keep this gap between your account’s paced sends. The call waits for its turn, up to 50 seconds. 0.1 – 3600. |
| daily_send_limit | integer | none | Deliver at most this many emails in any rolling 24 hours. Further calls are refused until the window frees up. |
All of these are optional. Boolean fields must be real true / false values — the string "false" is rejected with 400 rather than treated as true, so a typo can never switch on a charge.
Sending a saved template
Build the template in Templates, copy its id, and pass the merge-tag values in variables. Transactional mail never gets an unsubscribe footer appended.
curl -X POST https://pingovo.com/api/v1/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"from_email": "[email protected]",
"from_name": "Acme Support",
"templateId": "TEMPLATE_ID",
"variables": { "firstName": "Alex", "orderNumber": "1042" }
}'Response
| Field | Type | Description |
|---|---|---|
| request_id | string | Unique id of this API call. Quote it to support. |
| send_id | string | Id of this email’s record. Use it with GET /sends/:id to see delivery and engagement later. |
| data.to | string | The recipient. |
| data.subject | string | The subject that was sent. |
| data.message_id | string | The Message-ID header of the delivered email. |
| data.tags | string[] | The tags the send was recorded under. |
| credits.verification / .send / .tracking | number | The credits this call spent on each part. |
| credits_used | number | Total credits spent by this call. |
| credits_remaining | number | Your balance afterwards. |
| tracking.opens / .clicks | boolean | Which kinds of tracking were applied to this email. |
| delivery.delay_seconds / .waited_ms | number | Present when you used a pacing parameter: how long this call waited for its turn. |
Open and click tracking
Tracking is off unless you ask. Turn on track_opens, track_clicks or both and each tracked email costs one flat tracking charge — the same whether you enable one or both. It appears as its own line in your credit history, and it is taken only for an email that was actually delivered.
- Tracking needs an
htmlbody (or an HTML template). Asking for it without one fails with400 TRACKING_REQUIRES_HTMLbefore anything is charged. - If your balance cannot cover delivery and tracking, the call fails with
402 INSUFFICIENT_CREDITS. It is never silently downgraded to an untracked send. - Open counts run high, because some mail apps pre-load images. Treat clicks as the more reliable engagement signal. Bots and prefetchers are filtered out of both.
- Click links always point to the destinations in your email — they cannot be redirected anywhere else.
Delivery controls in practice
This example paces sends 30 seconds apart, caps the account at 500 delivered emails a day, skips risky and role-based addresses, tracks opens and clicks, and handles the refusals. Set your HTTP client's timeout above 50 seconds when you use delay_between_emails — the call is held open while it waits.
curl --max-time 90 -i -X POST https://pingovo.com/api/v1/send \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"subject": "Welcome aboard",
"html": "<p>Welcome! <a href=\"https://example.com/start\">Get started</a></p>",
"tags": ["onboarding"],
"track_opens": true,
"track_clicks": true,
"delay_between_emails": 30,
"daily_send_limit": 500,
"skip_risky": true,
"skip_role_based": true
}'What happens before a send
- The request is validated. Any malformed field fails fast with a 400 and costs nothing.
- The recipient is checked against your suppression list — addresses that bounced, unsubscribed or that you suppressed are never mailed (
422). - Unless switched off, the recipient is verified. An invalid address is not sent to. The verification is charged even if it leads to a skip.
- Your skip rules run (
skip_risky,skip_role_based). - The credits for the send are reserved. If delivery then fails, they are returned in full.
- The email is delivered, and you get the response above.
Refusals specific to /send
| Status | error | What it means |
|---|---|---|
| 400 | RECIPIENT_REQUIRED | to is missing or not a valid email address. |
| 400 | SUBJECT_REQUIRED | No subject was given for an html / text send. |
| 400 | CONTENT_REQUIRED | Give html, text, or a templateId. |
| 400 | INVALID_TAGS | More than 5 tags, a tag over 40 characters, or a disallowed character. |
| 400 | INVALID_TRACKING | track_opens / track_clicks was not true or false. |
| 400 | TRACKING_REQUIRES_HTML | Tracking was requested without an html body. |
| 400 | INVALID_DELIVERY_SETTINGS | delay_between_emails or daily_send_limit is out of range. |
| 400 | SKIP_REQUIRES_VERIFICATION | skip_risky / skip_role_based were used with verification switched off. |
| 400 | INVALID_FIELD_TYPE | A field has the wrong type, e.g. variables is not an object. |
| 400 | INVALID_SENDER | from_email is not a valid email address. |
| 400 | SENDER_NOT_FOUND | from_email is not a verified Sender on your account. Add and verify it under Senders first. |
| 400 | SENDER_NOT_SUPPORTED | SMTP senders cannot be used here. Use a domain or free-email sender. |
| 400 | TEMPLATE_RENDER_ERROR | The template could not be rendered with your variables. |
| 402 | INSUFFICIENT_CREDITS | Not enough credits. The response shows credits_required and credits_available. |
| 403 | — | Your account is not yet approved for campaign access. |
| 403 | API_KEY_REVOKED | Pingovo revoked this key. Contact support. |
| 404 | TEMPLATE_NOT_FOUND | templateId is not a template your account can use. |
| 422 | RECIPIENT_INVALID | The address failed verification and was not sent to. |
| 422 | RECIPIENT_SUPPRESSED | The address is on your suppression list, bounced before, or unsubscribed. |
| 422 | RECIPIENT_SKIPPED | One of your skip rules left the address out. skip_reason says which. |
| 429 | DAILY_LIMIT_REACHED | Your daily_send_limit is used up. The body has resets_at; Retry-After says how long to wait. Nothing is charged. |
| 429 | SEND_QUEUE_BUSY | Too many sends are already waiting behind delay_between_emails. Retry after Retry-After seconds. Nothing is charged. |
| 502 | VERIFICATION_ERROR | The pre-send verification could not be completed. Retry. |
| 502 | SEND_UNAVAILABLE | Sending is temporarily unavailable. Retry shortly. |
| 502 | SEND_FAILED | The receiving server refused the email. The body’s message carries its response and classification. |
Every one of these — including calls refused before anything was sent — is recorded and visible under Dashboard → API Sends with its error code, so you can see why a call did nothing.