curl --request GET \
--url https://logs.sendrapp.org/webhooks/{domain} \
--header 'Authorization: <api-key>'import requests
url = "https://logs.sendrapp.org/webhooks/{domain}"
headers = {"Authorization": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: '<api-key>'}};
fetch('https://logs.sendrapp.org/webhooks/{domain}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://logs.sendrapp.org/webhooks/{domain}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://logs.sendrapp.org/webhooks/{domain}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://logs.sendrapp.org/webhooks/{domain}")
.header("Authorization", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://logs.sendrapp.org/webhooks/{domain}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"events": [
{
"id": "<string>",
"timestamp": 123,
"type": "Reception",
"domain_name": "<string>",
"recipient": "jsmith@example.com",
"sender": "jsmith@example.com",
"from": "<string>",
"subject": "<string>",
"response_code": 123,
"response_content": "<string>",
"response_enhanced_code": "<string>",
"provider_name": "<string>",
"peer_name": "<string>",
"bounce_classification": "<string>",
"num_attempts": 123,
"size": 123,
"sendr_message_id": "<string>",
"direction": "<string>",
"campaign": "<string>",
"reception_protocol": "<string>",
"delivery_protocol": "<string>",
"tls_protocol_version": "<string>",
"tls_cipher": "<string>"
}
],
"count": 123,
"filters_applied": {},
"next_token": "<string>"
}{
"error": 123,
"message": "<string>"
}{
"error": 123,
"message": "<string>"
}{
"error": 123,
"message": "<string>"
}Get Email Logs
Retrieve email event logs for a domain associated with your tenant: receptions, deliveries, opens, clicks, temporary failures, bounces, and complaints. Results are returned newest-first and paginated via next_token.
curl --request GET \
--url https://logs.sendrapp.org/webhooks/{domain} \
--header 'Authorization: <api-key>'import requests
url = "https://logs.sendrapp.org/webhooks/{domain}"
headers = {"Authorization": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: '<api-key>'}};
fetch('https://logs.sendrapp.org/webhooks/{domain}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://logs.sendrapp.org/webhooks/{domain}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://logs.sendrapp.org/webhooks/{domain}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://logs.sendrapp.org/webhooks/{domain}")
.header("Authorization", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://logs.sendrapp.org/webhooks/{domain}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"events": [
{
"id": "<string>",
"timestamp": 123,
"type": "Reception",
"domain_name": "<string>",
"recipient": "jsmith@example.com",
"sender": "jsmith@example.com",
"from": "<string>",
"subject": "<string>",
"response_code": 123,
"response_content": "<string>",
"response_enhanced_code": "<string>",
"provider_name": "<string>",
"peer_name": "<string>",
"bounce_classification": "<string>",
"num_attempts": 123,
"size": 123,
"sendr_message_id": "<string>",
"direction": "<string>",
"campaign": "<string>",
"reception_protocol": "<string>",
"delivery_protocol": "<string>",
"tls_protocol_version": "<string>",
"tls_cipher": "<string>"
}
],
"count": 123,
"filters_applied": {},
"next_token": "<string>"
}{
"error": 123,
"message": "<string>"
}{
"error": 123,
"message": "<string>"
}{
"error": 123,
"message": "<string>"
}logs.sendrapp.org. Authenticate with your API key in the Authorization header.next_token.
Query parameters
All parameters are optional. Filters are combined with AND.| Parameter | Type | Description |
|---|---|---|
type | string | Single event type, exact match (see table below). Cannot combine multiple types in one request. |
message_id | string | Return all events for one message (its id). When set, other filters and pagination are ignored. |
recipient | string | Recipient address (substring match) |
sender | string | Envelope sender (substring match) |
from | string | Full From header (substring match) |
subject | string | Subject (substring match) |
response_code | integer | Exact response code, e.g. 421, 550 |
start_timestamp | integer | Window start, Unix seconds. Defaults to 15 days ago. |
end_timestamp | integer | Window end, Unix seconds. Defaults to now. |
limit | integer | Page size. Defaults to 50. |
next_token | string | Cursor from a previous response, to fetch the next page. |
type (e.g. Bounce) over a wide window scans a lot of history and can be slow. Narrow the search with start_timestamp / end_timestamp whenever possible.Event types
Filter with?type=<value>. One type per request.
type | Meaning | Error? |
|---|---|---|
Reception | Message accepted by Sendr for sending | no |
Delivery | Accepted by the recipient’s mail server | no |
Opened | Recipient opened the email | no |
Click | Recipient clicked a link | no |
TransientFailure | Temporary failure — will be retried | yes |
Delayed | Deferred and queued for a later retry | yes |
Bounce | Permanent failure — not retried | yes |
OOBBounce | Out-of-band bounce reported later by the receiver | yes |
Feedback | Spam complaint (feedback loop) | yes |
Response codes
response_code accompanies most events. 4xx are temporary (auto-retried), 5xx are permanent.
| Code | Class | Meaning |
|---|---|---|
250 | success | Accepted / delivered |
400 | temporary (internal) | Sendr-internal send error (e.g. connection dropped); also carried by Delayed events |
421 | temporary | Receiver is throttling / not accepting right now |
450 | temporary | Mailbox busy or “receiving mail too quickly” |
451 | temporary | Receiver processing error / greylisting |
452 | temporary | Receiver out of resources |
550 | permanent | Mailbox doesn’t exist or blocked as spam |
551 | permanent | User not local / relay denied |
552 | permanent | Mailbox full or message too large |
553 | permanent | Invalid recipient address |
554 | permanent | Transaction failed / blocked |
Pagination
When more results exist, the response includes anext_token. Pass it back as ?next_token=<token> (keeping the same filters) to fetch the next page. When next_token is absent, you’ve reached the end.
Example response
{
"events": [
{
"id": "1NyCaoz9siVg2-A-1782502911",
"timestamp": 1782502917,
"type": "TransientFailure",
"domain_name": "example.com",
"recipient": "user@yahoo.com",
"sender": "news@example.com",
"from": "Example <news@example.com>",
"subject": "Your weekly digest",
"response_code": 450,
"response_content": "User is receiving mail too quickly",
"response_enhanced_code": "",
"provider_name": "yahoo",
"peer_name": "mta6.am0.yahoodns.net",
"bounce_classification": "Uncategorized",
"num_attempts": 0,
"size": 165284,
"sendr_message_id": "1NyCaoz9siVg2-A-1782502911",
"direction": "outbound",
"delivery_protocol": "ESMTP",
"tls_protocol_version": "TLSv1_3"
}
],
"count": 1,
"filters_applied": { "type": "TransientFailure" },
"next_token": "eyJkb21haW5fbmFtZSI6..."
}
peer_name and TLS fields only appear once a connection to the recipient server was made).Errors
400 Bad Request: Missing domain in the path401 Unauthorized: MissingAuthorizationheader or invalid API key500 Internal Server Error: Unexpected server error
Authorizations
Path Parameters
Sender domain to retrieve logs for (must belong to your tenant)
Query Parameters
Filter by a single event type (exact match). Cannot combine multiple types in one request.
Reception, Delivery, TransientFailure, Delayed, Bounce, OOBBounce, Opened, Click, Feedback Return all events for a single message (its uuid / the id field). When set, other filters and pagination are ignored.
Filter by recipient address (substring match)
Filter by sender address (substring match)
Filter by the full From header (substring match)
Filter by subject (substring match)
Filter by exact SMTP/internal response code (e.g. 421, 550). Note: code 400 is an internal Sendr code, not a real SMTP reply.
Start of the time window, Unix seconds. Defaults to 15 days ago when omitted.
End of the time window, Unix seconds. Defaults to now when omitted.
Page size. Defaults to 50.
Opaque cursor from a previous response's next_token, used to fetch the next page.
Response
Logs retrieved successfully