On this page
BirdseyePost Customer Reporting API
Pull campaign details, mailing and scan counts, and recorded campaign spend into your dashboard, warehouse or spreadsheet integration.
Version: v1
API locations
Paths below are relative to your BirdseyePost app’s origin:
| Resource | Path |
|---|---|
| Reporting API | /api/v1/reporting |
| OpenAPI specification | /api/v1/reporting/openapi.json |
| API key management | /dashboard/settings/reporting-api |
| Web documentation | /developers/reporting |
Quick start
- Sign in to the correct account and open Settings → Reporting API. An account administrator creates the key.
- Name your key, choose its expiry, and enable campaign spend if needed. Save the key when it appears; it is shown once.
- Set
BIRDSEYEPOST_URLto your BirdseyePost app’s HTTPS origin and storeBIRDSEYEPOST_API_KEYas a server-side secret.
curl "$BIRDSEYEPOST_URL/api/v1/reporting/campaigns?limit=25&include=spend" \
-H "Authorization: Bearer $BIRDSEYEPOST_API_KEY"
All reporting requests use GET and JSON responses. The API reads the dashboard campaigns in the account bound to your key. Agent-ordering drafts use the separate ordering API. Sandbox keys cannot read account reporting.
Endpoints
Base path: /api/v1/reporting
| Method | Endpoint | Returns | Required permission |
|---|---|---|---|
| GET | /campaigns |
Paginated campaign list | reporting:read |
| GET | /campaigns/{id} |
One campaign | reporting:read |
| GET | /campaigns/{id}/spend |
Recorded spend by currency | reporting:read and spend:read |
Use a dashboard campaign ID returned by the campaign list in place of {id}.
Add include=spend to the campaign list or detail endpoint to retrieve campaigns and spend together. This requires both permissions. Without it, the response omits prices and payment data. The spend endpoint accepts no query parameters.
Campaign metrics
Metrics are lifetime totals:
| Field | Meaning |
|---|---|
mailpieces |
Generated pieces, including quantities represented by shared neighborhood QR codes. |
mailedMailpieces |
Pieces with a recorded mailing date, not confirmed deliveries. |
scans |
Scan events, not unique people or purchases. |
Recipient details, artwork files and internal billing notes are excluded.
Filters and pagination
The campaign list supports these query parameters:
| Parameter | Values | Meaning |
|---|---|---|
limit |
Integer from 1–100; default 25 | Maximum campaigns per page. |
after |
Previous response’s pagination.nextCursor |
Continue after that campaign ID. |
status |
Draft, Active, Paused, Completed, Archived |
Exact campaign status. |
createdFrom |
ISO timestamp with Z or a UTC offset |
Inclusive start for campaign creation. |
createdBefore |
ISO timestamp with Z or a UTC offset |
Exclusive end for campaign creation. |
include |
spend |
Include recorded campaign spend; requires spend:read. |
Results are ordered by ascending campaign ID. Keep filters unchanged between pages and stop when pagination.hasMore is false. Deleted campaigns are excluded. Pagination is a live view, so changes during an export can affect results.
Creation filters select campaigns. Spend remains the campaign’s lifetime recorded spend, not spend during the selected dates.
Detail requests only accept include=spend. Unknown or repeated parameters return 400.
Python pagination example
This example requires the requests package. Add retry handling for 429 and transient 503 responses in production integrations.
import os
import requests
url = os.environ["BIRDSEYEPOST_URL"].rstrip("/")
headers = {"Authorization": "Bearer " + os.environ["BIRDSEYEPOST_API_KEY"]}
params = {"limit": 100, "include": "spend"}
while True:
response = requests.get(url + "/api/v1/reporting/campaigns",
headers=headers, params=params, timeout=30)
response.raise_for_status()
page = response.json()
for campaign in page["data"]:
print(campaign["id"], campaign["spend"])
if not page["pagination"]["hasMore"]:
break
params["after"] = page["pagination"]["nextCursor"]
Understanding spend
All amounts use integer cents: 175000 means $1,750.00 in the returned currency. Keep USD and CAD separate; the API performs no currency conversion.
| Field | Meaning |
|---|---|
grossPaidCents |
Sum of recorded charge amounts with paid or refunded status. Includes the original amount of subsequently refunded payments. |
netPaidCents |
Recorded gross payments when no refund is recorded. null if any charge in that currency has a refund status, because exact refund amounts are unavailable. |
refundedCents |
0 when no refund is recorded; null when a refund is recorded. A refund status can represent a partial or full refund. |
pendingCents, failedCents, voidedCents |
Amounts associated with those charge statuses. They are excluded from gross paid spend and may represent repeated attempts. |
quote |
The campaign’s configured price. It is not proof of payment and is never substituted for paid spend. |
availability |
unavailable and an empty totalsByCurrency means there are no recorded charges. Historical or externally paid campaigns may fall into this category; it does not mean they were free. |
Spend comes from the same recorded campaign charges used by account billing. It is not a live Stripe reconciliation or an external invoice ledger. Payments and refunds appear after they have been recorded in BirdseyePost. No separate taxes, fees or unrecorded historical invoices are inferred.
Response example
Illustrative campaign list response with include=spend:
{
"data": [
{
"id": "campaign_example",
"name": "Autumn win-back",
"type": "Retention",
"format": "Postcard",
"mailPieceType": "Postcard 6x9",
"status": "Completed",
"frequency": "One-time",
"launchDate": "2026-10-01T00:00:00+00:00",
"createdAt": "2026-09-20T12:00:00+00:00",
"updatedAt": "2026-10-15T12:00:00+00:00",
"metrics": {
"mailpieces": 2500,
"mailedMailpieces": 2500,
"scans": 124
},
"spend": {
"source": "recorded_campaign_charges",
"availability": "recorded",
"quote": {
"amountCents": 175000,
"currency": "USD"
},
"chargeCount": 1,
"totalsByCurrency": [
{
"currency": "USD",
"grossPaidCents": 175000,
"refundedCents": 0,
"netPaidCents": 175000,
"pendingCents": 0,
"failedCents": 0,
"voidedCents": 0,
"refundStatus": "none_recorded",
"lastPaidAt": "2026-10-01T12:00:00+00:00"
}
]
}
}
],
"pagination": {
"hasMore": false,
"nextCursor": null
}
}
Errors and limits
Each account can make 60 reporting requests per UTC minute, shared across keys. A 429 response includes Retry-After: 60. Use exponential backoff for transient 503 errors.
Every reporting response includes X-Request-Id; include it when contacting support.
Example error response:
{
"error": {
"code": "insufficient_scope",
"message": "This connection needs spend:read."
},
"requestId": "example-request-id"
}
| HTTP status | Action |
|---|---|
| 400 | Correct your filters, date range or limit. |
| 401 | Supply a valid, unexpired key. Revocation or removal of the creator’s membership also invalidates access. |
| 403 | Use a live key with the required scopes. |
| 404 | The campaign is missing, deleted or not available to your account. |
| 405 | Use GET. |
| 429 | Wait for the Retry-After interval, then retry. |
| 503 | Wait and retry with exponential backoff. |
Security
Keys are tied to one account, expire automatically and can be revoked in Settings. BirdseyePost stores a hash of each key.
API requests must use the Authorization header over HTTPS; browser cookies do not grant reporting access. Keep keys in a server-side secret manager. Cross-origin browser access is not enabled.
To rotate a key:
- Create a replacement.
- Update your integration.
- Verify a request.
- Revoke the old key.
To integrate another account, use a separate key created by that account’s administrator.
Any other questions? Get in touch