Sending over the API
The send call, field by field, with what each answer means.
One POST, one JSON body, and an answer carrying the message's identifier. The
full reference is at API reference; this page is the explanation.
Authentication
An API key on every request, in the header.
Authorization: Bearer cconnect_xxxxxxxxxxxxxxxxxxxxxxxx
The key decides which account the request belongs to, which is why there is no account identifier in the URL or in the body. If the key is limited to a stream, it can only send through domains on that stream.
The smallest request
curl -X POST https://app.c-connect.pt/v1/messages \
-H "Authorization: Bearer $CCONNECT_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "receipts@mail.mycompany.com",
"to": "somebody@example.org",
"subject": "Your receipt",
"text": "Thank you for your purchase."
}'
Required are from, to, and a body: text, html, or both.
The fields
| Field | What it is |
|---|---|
from |
Any mailbox at a verified domain of yours. We check the domain, never the part before the @. |
from_name |
The name shown before the address. |
to |
One recipient. One message, one person. |
subject |
The subject line. |
text |
The plain text version. |
html |
The HTML version. |
headers |
Headers of your own, as an object. |
unsubscribe_url |
Required on the marketing stream. |
raw |
A complete MIME message you built. See Sending raw MIME. |
Always send text alongside html. A mail client that does not render HTML
shows the alternative, and a message with HTML alone scores worse with some
filters.
One recipient at a time
There is no field for several recipients, no cc, no bcc. That is
deliberate:
- each message has its own status, its own events and its own identifier, and a delivery that fails for one address says nothing about the others;
- a suppression is per address, and a message to five people of whom one is suppressed has no right answer;
ccon an automated message shows every recipient the others' addresses, which is a data leak nobody asked for.
To send to a thousand people, make a thousand requests. The API can take it.
The answer
{
"id": "01JB2XK7Q9R3N8Z6T4V0M5W1YC",
"message_id_header": "<01JB2XK7Q9R3N8Z6T4V0M5W1YC@c-connect.pt>",
"status": "queued",
"stream": "transactional",
"from": "receipts@mail.mycompany.com",
"to": "somebody@example.org",
"subject": "Your receipt",
"created_at": "2026-09-18T10:41:02+01:00"
}
The code is 202 Accepted, not 200. Accepted is not delivered: the message
was signed and queued, and what the receiver did with it arrives afterwards, as
events. An integration that treats 202 as "delivered" will lie to a user
sooner or later.
Keep the id. It is how you look the message up, it is what the webhooks carry,
and it is what support will ask for.
When it goes wrong
| Code | What happened | What to do |
|---|---|---|
401 |
Key missing, unknown or revoked. | Check the header and the panel. |
403 |
The key is limited to another stream. | Use the right key for this domain. |
422 |
The body failed validation, or the domain is not yours, or it is not verified, or the address is suppressed. | Read message and errors. |
429 |
Over the request limit. | Wait and retry. |
5xx |
Ours. | Retry with a growing delay. |
A 422 for a suppressed address is not a mistake of yours: it is the system
stopping a send that was going to bounce again. Treat it as a result rather than
as a failure.
Limits and retries
There are two ceilings, not one. The rate limit is per key, 600 a minute. The quota belongs to the whole account, per hour and per day, and it is the one that matters to anybody sending in batches: every key on the account spends the same one.
Every 202 carries what is left of it, so you neither have to ask nor find out
by hitting it:
X-CConnect-Quota-Window: hour
X-CConnect-Quota-Limit: 2000
X-CConnect-Quota-Remaining: 1873
X-CConnect-Quota-Reset: 1758186000
The window named is whichever is closest to running out, the hour or the day,
which is the one that decides when you can send the next one. Reset is in
seconds since the epoch.
A batch written well reads Remaining from the last answer and stops at zero,
instead of carrying on into the 429. To know the size of the batch before
starting it, GET /v1/me returns both windows.
The right answer to a 429 is to wait what Retry-After says rather than to
open more connections: more connections spend the same quota faster.
Retry a 5xx and a 429. Do not retry a 4xx: the request is the same and
the answer will be the same.
Retrying without sending twice
A 202 you already received and repeat sends a second message. That is not the
real problem; the other one is: a request that died halfway, or a connection
that dropped after we accepted and before you read the answer. Then you do not
know whether it went.
Put a key of your own on the request and the problem goes away:
curl -X POST https://app.c-connect.pt/v1/messages \
-H "Authorization: Bearer $CCONNECT_KEY" \
-H "Idempotency-Key: invoice-2026-001" \
-H "Content-Type: application/json" \
-d '{ "from": "invoices@mail.mycompany.com", "to": "customer@example.com", ... }'
One key per message, chosen by you. The identifier you already have on your side is usually the best one: the invoice number, the order id, the user id plus the action. A random key per request is worth nothing, because a retry would generate another.
What happens next:
| Second request | Answer |
|---|---|
| Same key, same body | The same 202 as before, with Idempotent-Replay: true. Nothing goes out. |
| Same key, different body | 409. The key already belongs to another message. |
| Same key, the first still running | 409 with Retry-After: 1. Try again in a moment. |
| No key | A second message goes out, as it always did. |
Keys belong to the account rather than to the API key, and last 24 hours. A
refused request gives its key back: if we answer 422 because the address is
suppressed, you can correct the recipient and repeat with the same key.