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:

FieldRequiredMeaning
package_idyesThe 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_ofnoWhen 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"
    }
  ]
}
ReasonWhat it means
not_sellerYour organization doesn’t sell this package, or the package doesn’t exist.
before_flight_startThe flight hasn’t started yet, or as_of is before the start date.
as_of_in_futureas_of is later than now.
quantity_requiredThe line needs impressions or downloads (whichever it’s priced on).
flat_rate_takes_no_quantityFlat-rate lines take no figure; send the item without one to mark it delivered.
duplicate_package_idThe same package appears twice in one request. Send one total per package per request.
invalid_itemThe item is malformed (bad package ID, negative number, both fields set, and so on).
internal_errorSomething went wrong on our side. Retry the item later.

Errors for the whole request

StatusMeaning
400The body isn’t valid JSON, or items is missing, empty or longer than 500.
401The key is missing, wrong or revoked.
413The body is too large.
429Too 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.