Sending
The route, the formats, the response.
POST https://pigeon.esse.dev/api/send-email, authenticated with the key of the project:
Authorization: Bearer pgn_sk_...
The key identifies the project, and the project decides the sender. projectId in the body
is not needed, and passing one different from the key's is a 400: it is a mistake on the
caller's side, not a parameter to ignore in silence.
With JSON
curl -X POST https://pigeon.esse.dev/api/send-email \
-H "Authorization: Bearer pgn_sk_..." \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"subject": "Subject",
"templateId": 12,
"templateData": { "name": "Mario" },
"replyTo": "[email protected]"
}'
to takes one address or a list, up to 50. replyTo is optional and does not change the
sender, which stays the one of the project.
With attachments
multipart/form-data, same fields, with two differences: to is comma separated and
templateData is a JSON string.
curl -X POST https://pigeon.esse.dev/api/send-email \
-H "Authorization: Bearer pgn_sk_..." \
-F "[email protected],[email protected]" \
-F "subject=The quote" \
-F "templateId=12" \
-F 'templateData={"name":"Mario"}' \
-F "[email protected]"
Do not send twice
The Idempotency-Key header carries an identifier of your choosing, one per message:
Idempotency-Key: order-4821-confirmation
The same key, on the same project, sends once and returns the same emailId. It is what you
want when you retry a call whose outcome you do not know, and it is what makes a retry after
a timeout safe: without it, Reliability asks you to choose between a
duplicate and a lost message.
The key is scoped to the project, so you have nobody to coordinate with: a UUID generated when the message is composed does the job, as long as you reuse it for every attempt of the same send and do not generate a new one for each.
A failed send does not lock the key: the same key retries on the same row until it works.
If instead an attempt with that key is still running, the answer is
409 IDEMPOTENCY_IN_PROGRESS, and it should be retried a few seconds later.
The response
An accepted request answers 200 with the id of the row in the history:
{ "emailId": 4821 }
Every failure answers 4xx or 5xx with the same shape:
{ "error": "Readable description", "code": "STABLE_CODE" }
No response has a success field: the HTTP status carries the outcome, and the code is
what you write a switch on. The message in error may change, the code does not.
Sending is synchronous: when the response arrives, the mail has already gone out or has already failed. There is no queue behind it, and that changes the way the caller has to be written: carry on with Errors and Reliability.
The exact schema of every field is in the OpenAPI documentation, generated from the code. It is the only link in this manual that leads outside: it is not a page, it is a schema rendered.