C C-Connect Documentation

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;
  • cc on 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.