NC

Naijacloud

Docs

DashboardStart free

API

/

Sending email

Sending email

Transactional email over plain REST at `https://api.naijacloud.com/v1`. An API key with the Email send scope is all it takes, and there are SDKs for Node, Python, Go, Ruby and PHP that wrap the same endpoints.

7 min read

Before you can send

Two things, in order:

  1. Verify a sending domain. You can only send from a domain the workspace has proved it owns. Add it under Email → Domains, publish the DNS records it shows you, and verify. We generate your DKIM keypair and sign messages ourselves, so those records are yours and ours — they do not change if we change mail providers behind the scenes.
  2. Create a key under Settings → API keys with the Email send scope.
i

Two credential shapes work here. An nc_live_ workspace key with Email send is the one to use — it is the same credential your CI already holds. An nmail_live_ key from the Email screen also works and is Naijamail-only. An nc_pat_ platform token does not: it predates the scope and is refused.

Send

POST /v1/emails

curl https://api.naijacloud.com/v1/emails \ -H "Authorization: Bearer $NC_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-4417-receipt" \ -d '{ "from": "Acme <[email protected]>", "to": ["[email protected]"], "subject": "Your receipt", "html": "<p>Thanks for your order.</p>", "text": "Thanks for your order.", "tags": { "kind": "receipt" } }'
FieldTypeRequiredNotes
fromstringyes"Name <[email protected]>" or "[email protected]". The domain must be verified
tostring or string[]yesAt least one address
cc, bccstring or string[]no
reply_tostring or string[]noreplyTo is accepted too
subjectstringnoDefaults to empty
html, textstringnoSend both where you can
headersmapnoUp to 25. From, To, Cc, Bcc, Subject, DKIM-Signature and Received are refused
attachmentsarrayno{ filename, content (base64), content_type?, content_id? }
tagsmapnoUp to 10; key ≤ 64 chars, value ≤ 256
idempotency_keystringnoThe Idempotency-Key header takes precedence

Limits: 50 recipients across to+cc+bcc, and 10 MiB encoded.

202 Accepted

{ "id": "5b1e…", "status": "queued", "rejected": [{ "address": "[email protected]", "reason": "suppressed" }] }

rejected appears only when non-empty. It is not an error — the rest went out — but it is the thing to log, because it is how you find out you are mailing addresses that have unsubscribed or hard-bounced.

!

Set an idempotency key on anything that costs money or embarrasses you twice. A retry with the same key returns the original message rather than sending again, which is what makes a timeout safe to retry.

Send a batch

Up to 100 distinct messages in one request — worth it from Lagos, where the round trip is most of what makes a loop feel slow.

POST /v1/emails/batch

curl https://api.naijacloud.com/v1/emails/batch \ -H "Authorization: Bearer $NC_API_KEY" \ -H "Content-Type: application/json" \ -d '[ { "from": "Acme <[email protected]>", "to": ["[email protected]"], "subject": "Receipt", "html": "<p>1</p>", "idempotency_key": "r-1" }, { "from": "Acme <[email protected]>", "to": ["[email protected]"], "subject": "Receipt", "html": "<p>2</p>", "idempotency_key": "r-2" } ]'

A batch is not atomic, on purpose: one malformed message must not discard the ninety-nine good ones. Each entry comes back as { id, status } or { error }, in request order, and the response is 202 whatever the mix — so walk the array rather than branching on the status code.

Idempotency is per item and yours to supply. We cannot generate one per item, because a retried batch would then get fresh keys and send everything twice.

Read a message back

GET /v1/emails/{id}

curl https://api.naijacloud.com/v1/emails/5b1e… \ -H "Authorization: Bearer $NC_API_KEY"

status is lowercase and one of queued, sent, delivered, bounced, deferred, complained, rejected, failed. Treat it as an open set — a new status should not crash your integration.

to on a retrieved message is a single address: we write one row per primary recipient, so a three-recipient send returns the id of the first and each recipient has its own record.

A message belonging to another workspace answers 404, exactly like one that never existed. Do not read a 404 as "this id is free".

List and filter

GET /v1/emails?status=bounced&since=2026-08-01&page=1&limit=50

curl "https://api.naijacloud.com/v1/emails?status=bounced&limit=50" \ -H "Authorization: Bearer $NC_API_KEY"

Filters: status, to, since, until. Every paginated endpoint returns the same envelope:

{ "data": [ ], "page": 1, "limit": 25, "total_count": 431, "has_more": true }

page is 1-based and limit is clamped to 100. A non-numeric page or limit is refused with a 400 rather than silently corrected.

Quotas

GET /v1/limits

curl https://api.naijacloud.com/v1/limits -H "Authorization: Bearer $NC_API_KEY"

Returns the static limits, your per-key rate limit and the per-domain daily quota. A new verified domain starts at 200 sends on its first day and the ceiling doubles per full day of clean sending, up to a self-serve maximum of 50,000. Reputation, not paperwork — check this endpoint before a large run rather than discovering the cap mid-send.

Domains and suppressions

These change configuration rather than send correspondence, so they need EMAIL_SEND and PLATFORM_API on the key. A send-only key gets a 403 naming both.

EndpointDoes
GET /v1/domainsList sending domains
POST /v1/domainsAdd one, and get the DNS records to publish
GET /v1/domains/{id}One domain with its records
POST /v1/domains/{id}/verifyRe-check DNS now
DELETE /v1/domains/{id}Remove it
GET /v1/suppressionsAddresses we will not mail
POST /v1/suppressionsAdd one. reason is never accepted here
DELETE /v1/suppressions/{address}Remove one

On a domain read, each record's verified and problem are null rather than false — a list does not resolve DNS, only the verify call does. Treat them as tri-state, or you will report a healthy domain as broken.

Deleting a complaint suppression answers 409, not 403. Someone marked your mail as spam; re-adding them is not something a retry can fix.

Errors

Standard status codes, with a NestJS body:

{ "statusCode": 403, "message": "not allowed to send from \"[email protected]\". Verify the domain first.", "error": "Forbidden" }

message is a string or an array of strings — handle both.

StatusMeaningRetry?
401Missing, revoked or malformed keyno
403Scope, unverified domain, or a test key on a live sendno
404No such message, or not yoursno
409Conflicts with existing state, e.g. a complaint suppressionno
422The body failed validationno
429Rate limited — honour Retry-Afteryes
5xxOursyes

Log the x-request-id response header. It is what support needs to find your exact send.

SDKs

Five official SDKs wrap all of this, with retries, idempotency and key redaction already handled — nc-email-node, nc-email-python, nc-email-go, nc-email-ruby and nc-email-php. All accept an nc_live_ workspace key:

Node

import { Naijamail } from '@naijacloud/email'; const nm = new Naijamail(process.env.NC_API_KEY); await nm.emails.send({ from: 'Acme <[email protected]>', to: ['[email protected]'], subject: 'Your receipt', html: '<p>Thanks for your order.</p>', });

They read NAIJAMAIL_API_KEY from the environment when no key is passed, and check the key's shape at construction — so a truncated paste fails at start-up rather than as a 401 during a customer's checkout.