Buyer quickstart

This page takes a buyer agent from discovery to a booked campaign, with its creative brief, on the public sales agent, https://salesagent.streetsdigital.com/. It uses nine AdCP calls. At the end there’s a script that runs them all for you and cancels the test buy afterwards.

What you need

  • A bearer key (crrbn_buyer_…) from the Carbnn team. Carbnn also opens products to your key and adds your brand as an account.
  • A signing key registered with Carbnn, for create_media_buy and update_media_buy (step 1).
  • Node.js 20 or later and npm install @adcp/sdk if you want to use the script or the SDK’s signer.

Every call is a JSON-RPC tools/call POST to the endpoint, with these headers:

Content-Type: application/json
Accept: application/json, text/event-stream
Authorization: Bearer <your-key>

The AdCP result is in result.structuredContent. An error comes back as result.structuredContent.adcp_error with a code, a message and often a field. See errors and limits.

1. Set up a signing key

Booking a media buy must be signed with RFC 9421 HTTP message signatures. Generate an Ed25519 key pair with the script from step 10:

node adcp-buyer-e2e.mjs keygen --kid my-buyer-2026-09 --out ./my-buyer.private.jwk

It writes the private key to the file (readable only by you) and prints the public JWKS. Host that JWKS at an https:// URL you control, then send the Carbnn team the URL and the key id (my-buyer-2026-09). Keep the private key secret; Carbnn never needs it.

Any AdCP request-signing key works: Ed25519 or ECDSA P-256, with adcp_use: "request-signing" and key_ops: ["verify"] on the public key.

2. Check capabilities

get_adcp_capabilities takes no arguments. Look for:

  • supported_protocols: media_buy, signals and creative.
  • request_signing.required_for: the tools that must be signed, create_media_buy and update_media_buy.
  • account.supported_billing: ["operator"]: Carbnn invoices you, and payment is settled outside the protocol.

3. Find signals

{ "name": "get_signals", "arguments": { "signal_spec": "UK comedy high engagement" } }

Returns Carbnn’s contextual signals (content category, engagement tier, audience region and age band), with coverage over the products opened to your key.

4. Pick an account

{ "name": "list_accounts", "arguments": {} }

Returns your brands, each with an account_id. You’ll pass one to create_media_buy.

5. Get products

{
  "name": "get_products",
  "arguments": { "buying_mode": "brief", "brief": "UK comedy podcasts with high engagement" }
}

Returns the products opened to your key. Prices include Carbnn’s buyer fee: fixed_price is what you pay, and price_breakdown shows the list price, the fee and the seller commission. Pooled products group several creators behind one product; their description says the roster may change until you book.

6. Look up the brief format

{
  "name": "list_creative_formats",
  "arguments": { "format_ids": ["<format_ids from the product>"] }
}

Each product lists its format_ids. The format’s assets say what the creator needs from you, for example a brief, a disclosure_text and a landing_url, and which of them are required.

7. Book, with the brief

Pick a product and one of its pricing options, then send create_media_buy, signed, with the brief in the package’s creatives:

{
  "name": "create_media_buy",
  "arguments": {
    "idempotency_key": "<a fresh UUID per buy>",
    "account": { "account_id": "<account_id from list_accounts>" },
    "brand": { "domain": "acme.example.com" },
    "po_number": "E2E test (demo)",
    "start_time": "2026-10-05T00:00:00Z",
    "end_time": "2026-10-12T00:00:00Z",
    "packages": [
      {
        "product_id": "<product_id>",
        "pricing_option_id": "<pricing_option_id>",
        "budget": 4725,
        "creatives": [
          {
            "creative_id": "autumn-brief-1",
            "name": "Autumn range brief",
            "format_id": {
              "agent_url": "https://salesagent.streetsdigital.com",
              "id": "<format id>"
            },
            "assets": {
              "brief": { "asset_type": "text", "content": "Mention the autumn range." },
              "disclosure_text": { "asset_type": "text", "content": "#ad" },
              "landing_url": { "asset_type": "url", "url": "https://acme.example.com/autumn" }
            }
          }
        ]
      }
    ]
  }
}
  • budget is what you pay, fee included, and must be at least the option’s min_spend_per_package.
  • Use a fresh idempotency_key per buy. If the call times out, send the same request with the same key: you get the original buy back, never a second one.
  • creatives is optional at booking: you can add the brief later with update_media_buy, but the buy can’t start until every package has an approved one. See creative briefs.
  • On a pooled or multi-creator product you can leave the split to Carbnn (seller-managed) or send your own in packages[].ext.carbnn.creator_allocations. See creator allocation.

With @adcp/sdk, signing is one call. Sign the exact body you send, for the exact URL you call:

import { signRequest } from '@adcp/sdk/signing/client';
 
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params });
const { headers: signature } = signRequest(
  { method: 'POST', url: 'https://salesagent.streetsdigital.com/', headers: {}, body },
  { keyid: 'my-buyer-2026-09', alg: 'ed25519', privateKey: myPrivateJwk },
  { coverContentDigest: true }
);
// POST with your usual headers plus `signature` (Signature, Signature-Input, Content-Digest).

A call with only the bearer key is refused with AUTH_REQUIRED and the reason request_signature_required.

8. Read the receipt

The response carries the media_buy_id, media_buy_status: "pending_creatives" and one entry per package:

  • price_breakdown: the list price and rates frozen on this buy. Later rate changes never alter it.
  • ext.carbnn.allocation (pooled and multi-creator products): how the budget was split across creators. mode is seller_managed when Carbnn made the split.
  • creative_assignments and ext.carbnn.creatives: your brief, with Carbnn’s id for it and its review status (pending_review until the creator approves it).

9. Follow, change or cancel the buy

{ "name": "get_media_buys", "arguments": { "media_buy_ids": ["<media_buy_id>"] } }

The buy starts as pending_creatives. The creator reviews the brief; once every package has an approved brief the buy moves to pending_start, and Carbnn confirms funding before it starts delivering. get_media_buys shows each change of status. get_media_buy_delivery reports clicks and conversions once it’s live.

To pause, resume, cancel, change a package budget or send a new brief, call update_media_buy, signed like the booking:

{
  "name": "update_media_buy",
  "arguments": {
    "idempotency_key": "<a fresh UUID per change>",
    "media_buy_id": "<media_buy_id>",
    "account": { "account_id": "<account_id>" },
    "canceled": true,
    "cancellation_reason": "No longer needed"
  }
}

See update_media_buy for everything it takes.

10. Run it end to end

adcp-buyer-e2e.mjs (in the Carbnn repository under scripts/) runs steps 2 to 9 in order and prints what it finds. It needs @adcp/sdk installed next to it.

export ADCP_BUYER_TOKEN=crrbn_buyer_…            # never printed
export ADCP_SIGNING_KEY=./my-buyer.private.jwk   # private JWK or PKCS#8 PEM
node adcp-buyer-e2e.mjs --dry-run                # stops before booking
node adcp-buyer-e2e.mjs                          # books a demo buy, then cancels it
node adcp-buyer-e2e.mjs --no-cancel              # books a demo buy and leaves it booked

Optional settings:

  • ADCP_AGENT_URL: the agent to call. Defaults to https://salesagent.streetsdigital.com/.
  • ADCP_SIGNING_KID: the key id, if your key file doesn’t carry one.
  • ADCP_ACCOUNT_ID: which account to book for. Defaults to the first.
  • ADCP_BUDGET: package budget. Defaults to the option’s minimum spend.
  • ADCP_IDEMPOTENCY_KEY: reuse a key to see an idempotent replay.

The script prefers a pooled product, fills the required assets of the product’s first brief-style format with an obvious test brief, books one week starting next Monday, names the buy E2E test (demo) <date>, and prints the receipt and the buy’s status. It then cancels the buy with update_media_buy (--cancel, on by default), so the test cleans up after itself. With --no-cancel the buy is a real order: cancel it yourself with update_media_buy if you don’t want it to run.