>

Account and platform

>

API

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

  1. Sign in to the correct account and open Settings → Reporting API. An account administrator creates the key.
  2. Name your key, choose its expiry, and enable campaign spend if needed. Save the key when it appears; it is shown once.
  3. Set BIRDSEYEPOST_URL to your BirdseyePost app’s HTTPS origin and store BIRDSEYEPOST_API_KEY as 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:

  1. Create a replacement.
  2. Update your integration.
  3. Verify a request.
  4. Revoke the old key.

To integrate another account, use a separate key created by that account’s administrator.

Choose which optional services can run. Essential cookies stay on. Privacy policy