Developer API
Programmatic access to your BannerTrackr data
Developer API
Access your BannerTrackr data programmatically. Use the API to integrate with your own dashboards, automate workflows, or build custom integrations.
The Developer API is available on Starter, Growth, and Pro plans.
Quick Start
1. Create an API Key
- Go to Settings → Developer API in your dashboard
- Click Create Key
- Give your key a name and select permissions:
- Read Only — View sites, placements, and stats
- Read & Write — Also create, update, and delete resources
- Copy your key immediately — it won't be shown again
2. Make Your First Request
curl -X GET "https://bannertrackr.com/api/public/v1/me" \
-H "Authorization: Bearer bt_live_your_key_here"Authentication
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer bt_live_xxxxxxxxxxxxxKeys are prefixed with bt_live_ for easy identification.
Rate Limits
Rate limits are based on your plan:
| Plan | Read Requests | Write Requests |
|---|---|---|
| Starter | 60/min | 10/min |
| Growth | 120/min | 30/min |
| Pro | 300/min | 60/min |
Rate limit headers are included in every response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1704067200When rate limited, you'll receive a 429 response with a Retry-After header.
Pagination
List endpoints use cursor-based pagination:
GET /api/public/v1/placements?limit=50&cursor=abc123Response:
{
"data": [...],
"pagination": {
"nextCursor": "xyz789",
"hasMore": true
}
}limit: 1-100, default 50cursor: PassnextCursorfrom previous response
Endpoints
Account
Get Current User
GET /api/public/v1/meReturns your account info, plan, and usage limits.
Response:
{
"data": {
"id": "user_123",
"email": "you@example.com",
"name": "Your Name",
"plan": "growth",
"createdAt": "2024-01-15T10:30:00Z",
"usage": {
"sites": 5,
"sitesLimit": 10,
"apiKeys": 2,
"apiKeysLimit": 5
},
"limits": {
"impressionsPerMonth": 500000,
"clicksPerMonth": 50000,
"apiRequestsPerMinute": 120
}
}
}Sites
List Sites
GET /api/public/v1/sitesQuery Parameters:
limit— Results per page (1-100, default 50)cursor— Pagination cursor
Response:
{
"data": [
{
"id": "site_123",
"name": "My Newsletter",
"type": "newsletter",
"domain": "newsletter.example.com",
"placementCount": 5,
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-20T14:00:00Z"
}
],
"pagination": {
"nextCursor": null,
"hasMore": false
}
}Get Site
GET /api/public/v1/sites/:idReturns the same fields as the list response for a single site.
Create Site
POST /api/public/v1/sitesRequires: read_write scope
Body:
{
"name": "My Newsletter",
"type": "newsletter",
"domain": "newsletter.example.com"
}| Field | Required | Description |
|---|---|---|
name | Yes | Site name |
type | Yes | "website" or "newsletter" |
domain | No | Optional domain |
Update Site
PUT /api/public/v1/sites/:idRequires: read_write scope
Body: Any fields from Create Site (all optional for updates).
Delete Site
DELETE /api/public/v1/sites/:idRequires: read_write scope
Response:
{
"success": true,
"message": "Site deleted",
"deletedPlacements": 5
}Deleting a site also deletes all its placements and tracking data.
Placements
List Placements
GET /api/public/v1/placementsQuery Parameters:
siteId— Filter by sitestatus— Filter by status (active,paused,expired)limit— Results per page (1-100, default 50)cursor— Pagination cursor
Response:
{
"data": [
{
"id": "plc_123",
"name": "Header Banner - Acme Corp",
"pixelId": "abc123def456",
"destinationUrl": "https://acme.com",
"hostedImageUrl": "https://cdn.bannertrackr.com/...",
"status": "active",
"startDate": "2024-01-01T00:00:00Z",
"endDate": "2024-03-31T23:59:59Z",
"renewalDate": "2024-03-15T00:00:00Z",
"rate": 500,
"billingModel": "flat",
"currency": "USD",
"utm": {
"source": "newsletter",
"medium": "email",
"campaign": "q1-2024",
"term": null,
"content": null
},
"site": {
"id": "site_123",
"name": "My Newsletter"
},
"advertiser": {
"id": "adv_123",
"name": "John Smith",
"email": "john@acme.com",
"companyName": "Acme Corp"
},
"trackingUrls": {
"pixel": "https://bannertrackr.com/api/track/pixel.gif?p=abc123def456",
"image": "https://bannertrackr.com/api/track/img/abc123def456",
"click": "https://bannertrackr.com/api/track/click/abc123def456"
},
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-15T00:00:00Z"
}
],
"pagination": {
"nextCursor": null,
"hasMore": false
}
}Get Placement
GET /api/public/v1/placements/:idReturns the same fields as the list response:
{
"data": {
"id": "plc_123",
"name": "Header Banner - Acme Corp",
"pixelId": "abc123def456",
"trackingUrls": {
"pixel": "https://bannertrackr.com/api/track/pixel.gif?p=abc123",
"image": "https://bannertrackr.com/api/track/img/abc123",
"click": "https://bannertrackr.com/api/track/click/abc123"
}
}
}Tracking URL Types:
pixel— 1x1 transparent GIF for impression tracking (use in<img>tags)image— Serves your hosted image while tracking impressions (for newsletters)click— Click tracking with redirect to destination URL (wrap your links)
Create Placement
POST /api/public/v1/placementsRequires: read_write scope
Body:
{
"siteId": "site_123",
"name": "Header Banner",
"destinationUrl": "https://example.com",
"status": "active",
"startDate": "2024-01-01",
"endDate": "2024-03-31",
"renewalDate": "2024-03-15",
"rate": 500,
"billingModel": "flat",
"currency": "USD",
"advertiserId": "adv_123",
"utm": {
"source": "newsletter",
"medium": "email",
"campaign": "spring-2024",
"term": "banner",
"content": "header"
}
}| Field | Required | Description |
|---|---|---|
siteId | Yes | ID of the site this placement belongs to |
name | Yes | Placement name |
destinationUrl | No | Where clicks redirect to |
status | No | "active" or "paused" (default: "active") |
startDate | No | Campaign start date (ISO 8601) |
endDate | No | Campaign end date (ISO 8601) |
renewalDate | No | Renewal reminder date (ISO 8601) |
rate | No | Billing rate (e.g., 500) |
billingModel | No | "flat", "cpm", "cpc", or "trade" |
currency | No | Currency code (default: "USD") |
advertiserId | No | Link to an advertiser (must exist in your account) |
utm | No | UTM parameters to append to click URLs |
The advertiserId must reference an advertiser you've already created in your dashboard. Advertisers cannot be created via the API.
Update Placement
PUT /api/public/v1/placements/:idRequires: read_write scope
Body: Any fields from Create Placement except siteId (all optional). Set a field to null to clear it.
Delete Placement
DELETE /api/public/v1/placements/:idRequires: read_write scope
Response:
{
"success": true,
"message": "Placement deleted"
}Statistics
Get Placement Stats
GET /api/public/v1/placements/:id/statsQuery Parameters:
from— Start date (YYYY-MM-DD), default: 30 days agoto— End date (YYYY-MM-DD), default: todaybreakdowns— Include device/country breakdowns (true/false)
Response:
{
"data": {
"placementId": "plc_123",
"placementName": "Header Banner",
"period": {
"from": "2024-01-01",
"to": "2024-01-31"
},
"summary": {
"impressions": 50000,
"clicks": 1500,
"uniqueVisitors": 35000,
"ctr": 3.0
},
"daily": [
{
"date": "2024-01-01",
"impressions": 1200,
"clicks": 36,
"uniqueVisitors": 900,
"ctr": 3.0
}
],
"breakdowns": {
"byDevice": [
{ "device": "desktop", "impressions": 30000, "clicks": 900 },
{ "device": "mobile", "impressions": 18000, "clicks": 540 },
{ "device": "tablet", "impressions": 2000, "clicks": 60 }
],
"byCountry": [
{ "country": "US", "impressions": 25000, "clicks": 750 },
{ "country": "GB", "impressions": 10000, "clicks": 300 }
]
}
}
}The breakdowns field is only included when breakdowns=true is passed. Country breakdown returns the top 10 countries by impressions.
Error Handling
All errors return a consistent format:
{
"error": "Error message here",
"hint": "Optional helpful suggestion"
}HTTP Status Codes
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad request (validation error) |
| 401 | Unauthorized (invalid/missing key) |
| 403 | Forbidden (wrong scope or plan limit) |
| 404 | Not found |
| 429 | Rate limit exceeded |
| 500 | Server error |
Limit Exceeded Errors
When you hit a plan limit (sites, placements, etc.), you'll receive a 403 response with additional details:
{
"error": "Site limit reached",
"limit": 3,
"current": 3,
"upgradeUrl": "/pricing"
}Scope Errors
If your API key doesn't have write permissions:
{
"error": "This API key is read-only",
"hint": "Create a key with read_write scope to perform this operation"
}Code Examples
JavaScript / Node.js
const API_KEY = 'bt_live_your_key_here';
const BASE_URL = 'https://bannertrackr.com/api/public/v1';
async function getStats(placementId, from, to) {
const params = new URLSearchParams({ from, to, breakdowns: 'true' });
const response = await fetch(
`${BASE_URL}/placements/${placementId}/stats?${params}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
},
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || `API error: ${response.status}`);
}
return response.json();
}
// Usage
const stats = await getStats('plc_123', '2024-01-01', '2024-01-31');
console.log(`CTR: ${stats.data.summary.ctr}%`);Python
import requests
API_KEY = 'bt_live_your_key_here'
BASE_URL = 'https://bannertrackr.com/api/public/v1'
def get_stats(placement_id, from_date, to_date):
response = requests.get(
f'{BASE_URL}/placements/{placement_id}/stats',
headers={'Authorization': f'Bearer {API_KEY}'},
params={
'from': from_date,
'to': to_date,
'breakdowns': 'true'
}
)
response.raise_for_status()
return response.json()
# Usage
stats = get_stats('plc_123', '2024-01-01', '2024-01-31')
print(f"CTR: {stats['data']['summary']['ctr']}%")Managing API Keys
- Create up to 5 active keys per account
- Revoke keys immediately from Settings → Developer API
- Keys are scoped: use Read Only when you only need to fetch data
- If you downgrade to the Free plan, all keys are automatically revoked
Use separate keys for different environments (production, staging, CI/CD).