Delivery API
If your hosting platform or ad server already knows how many impressions or downloads a campaign has delivered, you can send those figures to carbnn automatically instead of typing them into Report delivery by hand. The API applies the same rules as the form, so buyers see the same thing either way.
1. Create an API key
An organization admin creates keys under Settings → API keys.
- Give the key a label (for example, the system that will use it) and click Create key.
- The full key is shown once. Copy it into your system’s secret store straight away. carbnn keeps only a fingerprint of it, so a lost key can’t be recovered: revoke it and create a new one.
- The list shows each key’s first few characters, label, when it was created and when it was last used.
- Revoke stops a key working immediately.
A key only ever acts for the organization that created it. Treat it like a password: don’t put it in a browser, a public repo or a shared document.
2. Find the package ID
Each line of a campaign you’re selling has a package ID. Open the campaign, find your line, and copy the API package ID shown under Report delivery.
3. Send delivery figures
Keep the key out of your scripts: put it in an environment variable, e.g. export CARBNN_API_KEY=....
curl -X POST https://carbnn.streetsdigital.com/api/ingest/delivery/metrics \
-H "Authorization: Bearer $CARBNN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "package_id": "5f0c2a1e-8b7d-4c3a-9e21-a4b6c8d0e2f1", "impressions": 125000, "as_of": "2026-09-29" },
{ "package_id": "7e3d9b40-2c1f-4a8e-b5d6-c0e1f2a3b4c5", "downloads": 4200 }
]
}'Each item takes:
| Field | Required | Meaning |
|---|---|---|
package_id | yes | The API package ID from step 2. |
impressions | * | Total impressions delivered to date (a whole number). |
downloads | * | Total downloads delivered to date, for podcast and other download-priced lines. |
as_of | no | When the total was measured: a date (2026-09-29) or a timestamp with a time zone. Defaults to the time we receive it. |
* Send impressions or downloads (whichever the line is priced on),
never both. For a flat-rate line, send neither: the item marks it delivered.
Send up to 500 items per request.
Always send a running total
Every figure is the total delivered so far, not what’s new since your
last update. The most recent figure for a line replaces the earlier ones,
so sending the same total twice is harmless and a missed update fixes itself
on the next one. Figures are ordered by as_of, so a late-arriving older
total never overwrites a newer one.
What gets refused
Each item is accepted or rejected on its own, so one bad line never blocks the rest. The response looks like this:
{
"accepted": 1,
"rejected": 1,
"results": [
{ "index": 0, "package_id": "5f0c2a1e-8b7d-4c3a-9e21-a4b6c8d0e2f1", "status": "accepted" },
{
"index": 1,
"package_id": "7e3d9b40-2c1f-4a8e-b5d6-c0e1f2a3b4c5",
"status": "rejected",
"reason": "not_seller"
}
]
}| Reason | What it means |
|---|---|
not_seller | Your organization doesn’t sell this package, or the package doesn’t exist. |
before_flight_start | The flight hasn’t started yet, or as_of is before the start date. |
as_of_in_future | as_of is later than now. |
quantity_required | The line needs impressions or downloads (whichever it’s priced on). |
flat_rate_takes_no_quantity | Flat-rate lines take no figure; send the item without one to mark it delivered. |
duplicate_package_id | The same package appears twice in one request. Send one total per package per request. |
invalid_item | The item is malformed (bad package ID, negative number, both fields set, and so on). |
internal_error | Something went wrong on our side. Retry the item later. |
Errors for the whole request
| Status | Meaning |
|---|---|
400 | The body isn’t valid JSON, or items is missing, empty or longer than 500. |
401 | The key is missing, wrong or revoked. |
413 | The body is too large. |
429 | Too many requests. Wait the number of seconds in the Retry-After header. |
Each key can make up to 60 requests a minute. Most feeds only need to send an update hourly or daily.
Affiliate codes and metrics
The same key also authenticates POST /api/ingest/affiliate/codes and
POST /api/ingest/affiliate/metrics — send an affiliate/referral feed’s
codes and click/conversion figures the same way you send delivery. A key
only ever writes codes and metrics for packages and codes your organization
sells; another organization’s rows are refused.
curl -X POST https://carbnn.streetsdigital.com/api/ingest/affiliate/codes \
-H "Authorization: Bearer $CARBNN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"codes": [
{ "packageId": "5f0c2a1e-8b7d-4c3a-9e21-a4b6c8d0e2f1", "code": "ACME-POD01", "slug": "acmepod01" }
]
}'
curl -X POST https://carbnn.streetsdigital.com/api/ingest/affiliate/metrics \
-H "Authorization: Bearer $CARBNN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"metrics": [
{ "code": "ACME-POD01", "date": "2026-09-29", "clicks": 120, "conversions": 4, "conversionValue": 199.96 }
]
}'Both take up to 1,000 rows per request, and each row is accepted or rejected
on its own (same { "ok": true, "upserted": N, "errors": [...] } shape as
before). A row is refused with a 400-level per-item error when its
packageId (codes) or resolved code (metrics) doesn’t belong to a package
your key’s organization sells.