Sign in

API reference

The Baseline package (the passive tap)

The wire the Baseline package runs on: one endpoint, a bearer tap key, the fifteen-field event, and what the server answers.

On this page

The Baseline package is the passive tap. Install Recovea beside the provider client you already use. Seven days of reporting later, your own numbers: by agent or workload if you run your own, by client or customer if they pay for what your AI does. Your calls keep going straight to your provider. Your application reports the envelope of each call to Recovea beside that path, after the call has returned: never a prompt, never a completion, never a provider key. Nothing routes through Recovea. The Baseline reports over this wire, and the platform shows what it found.

This page is the exact shapes, for the engineer who checks what leaves the process. Section 8 prints every batch on your own machine, with no key and nothing sent, if you would rather read it than read about it.

1 · What the tap does, and what it can never do

  • Install Recovea beside the provider client you already use. tap(client) returns the same client you passed in, wrapped, with the same behaviour. Nothing else in your code changes.
  • Your call is your call. The request goes to your provider on your own path. The wrapper does not change the request, the response, or the stream you see.
  • Reported on the side. After the call returns, the wrapper builds one event from the response envelope and queues it. Delivery is asynchronous and batched: it never delays your call, never fails it, and never throws into your code.
  • Never content. Not a prompt, not a message, not a completion, not tool arguments, not any content field. The wrapper does not read them, does not log them, and does not serialize them.
  • Never a provider key. The tap never sees a provider key and we never ask for one. There is no usage-endpoint key, no admin key and no read-only key on this path.
  • Nothing routes through Recovea. There is no proxy in the call path. If our endpoint is down, your call does not know.

Two enforcements, one on each side of the wire. The package applies the field list below when it serializes, so only those fields can leave your process. The server applies it again on arrival, and a field it does not know refuses the whole batch rather than trimming it. Neither side is trusted to be the only one holding the rule.

2 · The endpoint and the key

HTTP
POST /tap/v1/events HTTP/1.1
host: platform-api.recovea.ai
authorization: Bearer rcv_tap_…
content-type: application/json
user-agent: your-app/1.2.3

Send a user-agent. Both published packages do. The host sits behind a web firewall whose managed rules can refuse a request that omits one, and that refusal is a bare 403 from the edge: it never reaches the endpoint and carries none of the bodies below.

Rule Value
Endpoint POST https://platform-api.recovea.ai/tap/v1/events
Host platform-api.recovea.ai, not the platform's host
Auth Authorization: Bearer <tap key>
Key spelling rcv_tap_ followed by 32 random bytes as 64 hex characters
Minted one key per organization, when the Baseline is created
Shown once, in the platform, at mint and at rotation, through the read below
Stored as HMAC-SHA-256 under the server pepper, through the same function the platform's gateway keys use; the first 12 characters are kept in clear so the platform can show you which key a line is using
Held in clear from the mint until your first read of it, and never again. Rotation opens the same one-read window on the new key and closes it the same way
Rotation in the platform, on the Baseline's own page: the new key is shown once and the old key stops working at that moment
Session, CSRF, role none. The route carries no platform session and no role check; the key is the whole authorization
Key, from the environment RECOVEA_TAP_KEY
Endpoint, from the environment RECOVEA_TAP_ENDPOINT, defaulting to the endpoint above
Inspect, from the environment RECOVEA_TAP_INSPECT: print each batch, send nothing, no key needed. Section 8

The key names your Baseline. The server hashes the key you present, finds your Baseline's row from it, and takes your organization from that row. So nothing in the body has to name you, and nothing in the body can name anybody else: there is no account field to get wrong and none to forge.

Reading it, once

Recovea holds your tap key in clear from the moment it is minted until the first time you look at it, and never again. The read itself clears it, leaving the peppered hash and the 12-character prefix.

The read is a platform route, authenticated by your platform session, and not reachable with the tap key itself:

HTTP
GET /platform/v1/assessments/{id}/tap-key
JSON
{ "key": "rcv_tap_…", "prefix": "rcv_tap_9f2a" }

Ask a second time and it is a 404 with error.code tap_key_already_shown.

POST /platform/v1/assessments/{id}/tap-key/rotate answers the same {key, prefix} shape, once, on the same terms: the new key is readable until you read it, the old hash is replaced, and the old key stops working at that moment.

So there is one way back to a working key whether you never copied it or copied it and lost it: rotate, and copy the new one. There is no call that shows you the old one, and support cannot read it out to you, because after your first read nobody at Recovea has it either.

A wrong or missing key is 401, with no detail:

JSON
{
  "error": {
    "code": "unauthorized",
    "message": "The tap key was missing or not recognized."
  }
}

It does not say whether the key is unknown, revoked or mistyped, and it never echoes what was presented.

3 · The request body

JSON
{
  "events": [
    // one to five hundred of them, each exactly as in section 4
  ]
}
Rule Value
Top-level shape one key, events, and no other
Events per batch 1 to 500
Body size 256 KiB; above it the batch is refused 413
Content type application/json

An object with a second top-level key is refused whole, the same as an event with a field the table does not name. The published packages post at most 100 events per batch.

4 · The event: recovea-tap-v1

One whole event, as the package sends it:

JSON
{
  "schema": "recovea-tap-v1",
  "provider": "openai",
  "model": "gpt-5",
  "request_id": "chatcmpl-9x7QvK2tRbN4",
  "input_tokens": 3184,
  "output_tokens": 512,
  "cached_tokens": 2048,
  "stop_reason": "stop",
  "tool_calls": 2,
  "status": 200,
  "retries": 0,
  "latency_ms": 1274,
  "rate_limit": {
    "requests_limit": 500,
    "requests_remaining": 118,
    "tokens_limit": 800000,
    "tokens_remaining": 412300
  },
  "timestamp": "2026-09-18T04:12:07.412Z",
  "tag": "matter-4821"
}

If a field is not in this table, it is not in the object. The validator is built to match the table, and an object carrying a field the table does not name is refused whole.

Field Type Rule
schema string exactly recovea-tap-v1
provider string enum openai · anthropic · google · xai · mistral · other
model string 1 to 128 characters, [A-Za-z0-9._:/-]; the model the provider says it served, not the one you asked for
request_id string 1 to 128 characters, [A-Za-z0-9._:-]; the provider's response id or request-id header, and a UUID the package generates when the provider gives neither
input_tokens integer 0 to 1,000,000,000
output_tokens integer 0 to 1,000,000,000
cached_tokens integer 0 to 1,000,000,000; default 0
stop_reason string enum stop · length · tool_calls · content_filter · error · other
tool_calls integer 0 to 10,000. The count, never the arguments
status integer 100 to 599, the provider's HTTP status. 599 is the sentinel for a call that failed with no HTTP response at all, and 200 is assumed where the provider's SDK hands back a successful response and no status
retries integer 0 to 100, the provider call's own retry count. Always 0 from version 1 of the packages: no provider SDK exposes one, and the field is on the wire so a later version can fill it without a schema change
latency_ms integer 0 to 3,600,000
rate_limit object or null exactly requests_limit, requests_remaining, tokens_limit, tokens_remaining; each an integer 0 to 1,000,000,000 or null. No other key. Null when the SDK does not expose the headers
timestamp string RFC 3339, with a timezone. Refused more than 5 minutes in the future or more than 30 days in the past
tag string 1 to 64 characters, [A-Za-z0-9._:-]. Your own unit id: a matter, a claim, a client, an agent, a workload

Two of those fields say less today than the schema allows. Read a 0 in retries as not reported, never as no retry happened. status carries the provider's HTTP status where there is one, 599 where the call failed without ever getting a response, and 200 where the SDK hands back a success and no status.

tag is the only free-text field on this wire, and the only field a human types. Everything else is filled by the package from the provider's own response, typed and bounded. That is why the tag is short, restricted to a plain identifier, and validated when you wrap the client rather than on the call: an invalid tag throws at tap() time, before a single request has run, so you find out at start-up and not in production. One wrapped client carries one tag.

This tag is not the x-recovea-tags header. That header belongs to the gateway, has four namespaces and a 128-character value (Tags). The tap's tag is one plain identifier per wrapped client.

5 · What the server does with a batch

The batch The answer
Valid 202 with {"accepted": n}, where n is the number of events stored
An unknown key at any level, a wrong type, a number out of range, a bad character, a wrong schema, or an events array outside 1 to 500 400, the whole batch, never the offending event alone
Over 256 KiB 413, the whole batch, the same way
A key we do not know 401, with no detail
A key belonging to an organization that asked to be erased 401, the same bytes as the row above: the erasure revokes the stored key, so it is no longer a key we know
A key whose Baseline has closed 410, nothing stored. Section 5a
A request_id already stored for this Baseline the event is ignored, not an error; the batch still answers 202, and the ignored event is not counted in accepted

The 400 message names the first offending path and a short reason from a fixed list, and never echoes a value you sent; for a field the schema does not know it names the enclosing object, $ or events[i], rather than the stray key, because naming the key would echo something you sent.

JSON
{
  "error": {
    "code": "tap_invalid",
    "message": "events[3].latency_ms: is outside its published range"
  }
}

An oversized body is refused the same way, with its own code:

JSON
{
  "error": {
    "code": "tap_too_large",
    "message": "body over 256 KiB"
  }
}

A rejected payload is not logged anywhere. Not in an error log, not in a metric label, not in an exception report. It is refused and dropped. That is why the refusal is whole-batch rather than per-event: trimming an unknown field would mean reading it first.

Whole-batch is not data loss. The package retries a failed post twice, after 200 ms and after 800 ms, and then drops it, so a batch refused for a shape error costs the events in that batch and nothing else. It is also, deliberately, loud: an integration that is sending something it should not stops working rather than quietly half-working.

5a · The 410

A Baseline lasts seven calendar days. Your seven days are counted from the first whole day your application reports priced calls. The page shows the days as they pass; if no such day arrives within sixty days, the Baseline closes then with what it has. After it closes, a batch on its key is answered 410, nothing is stored, and the body's code says why:

code What the packages do
baseline_closed print one line, drop the queue, stop posting, then re-check about once a minute
any other code print one line, drop the queue, and stop posting for the life of the process

The baseline_closed body carries its sentence:

A batch on a closed Baseline
{
  "error": {
    "code": "baseline_closed",
    "message": "This Baseline has closed. Start a plan to keep reporting."
  }
}

A baseline_closed stops the reporting; it does not end it. The re-check rides your application's own calls, so it costs nothing while you are idle. When the key is admitted again, reporting starts on its own within about a minute of your application's next call, with no restart and nothing to re-integrate, and no second line is printed. What resumes is new calls only; events dropped while the Baseline was closed are gone, because they were never accepted.

An erasure is not a 410. It answers 401, exactly as an unknown key does: the erasure removes the stored hash of the key. What the holder sees is the 401 ladder: after three consecutive 401s the package prints recovea tap: the tap key was not accepted three times; reporting stopped, drops its queue and goes quiet for the life of the process.

In every one of these endings the wrapped client goes on working exactly as it did.

6 · What a figure is called

Every money and token figure the platform shows from these events carries one of three labels, and the label is part of the figure.

Label What it means
reported tokens as the provider's usage field, reported by your own client. The three token fields above are reported figures
modeled cost from Recovea's frozen reference price list applied to reported tokens, with the price basis printed beside it. No cost is ever sent over this wire; it is computed when the figure is read
declared a figure you typed

A tap figure is never called measured. An event is emitted by your own process, from your provider's own response, and we did not stand in the path to watch it happen. That makes it your report of the call, which is a different and weaker kind of evidence than a record we kept ourselves, and the platform says so rather than rounding it up to a measurement.

A model with no price on the reference list produces no modeled cost: the figure is null and the platform says there is no reference price for that model. It is never guessed and never filled in from a neighbouring model.

There are no receipts over these events, and no hash chain over them. What the platform claims is the line at the foot of the Baseline page:

Computed from the events your own application reported. Export them and check the arithmetic.

7 · The install

JavaScript
import OpenAI from "openai";
import { tap } from "recovea";

const openai = tap(new OpenAI(), { tag: "matter-4821" });
// use openai exactly as before
Python
from openai import OpenAI
from recovea import tap

client = tap(OpenAI(), tag="matter-4821")

The packages are recovea on npm and recovea on PyPI. The key comes from RECOVEA_TAP_KEY and the endpoint from RECOVEA_TAP_ENDPOINT unless you pass them in options. Both post to the endpoint in section 2, with the body in section 3, carrying the events in section 4. There is nothing else on the wire.

With no key configured the wrapped client still works and reports nothing, with one line to stderr when you construct it, unless inspect mode is on, which needs no key and prints instead.

8 · Inspect mode: read the batch before it leaves

Turn it on and both packages print every batch to stderr as the exact JSON body this endpoint would have received, and send nothing. There is no request at all. It is here so the engineer doing the checking can read the fifteen fields on their own machine, at their own traffic, before any of them leave it.

Shell
$ RECOVEA_TAP_INSPECT=1 node ./your-script.js
[recovea] tap inspect mode is on: each batch is printed below as the exact JSON body the endpoint would receive, and nothing is sent.
{"events":[{"schema":"recovea-tap-v1","provider":"openai","model":"gpt-5",…}]}
Rule Value
Environment RECOVEA_TAP_INSPECT set to 1, true, yes or on. Surrounding spaces and case are ignored; anything else, and unset, is off
Option inspect: true in JavaScript, inspect=True in Python. A boolean wins over the environment in both directions, so inspect: false turns the variable off for one wrapped client
A key not needed. Inspect mode enables the reporter on its own
What is printed one line per batch: the body of section 3, up to 100 events, with no spaces and no newlines inside it, the bytes this endpoint receives
Where stderr, never stdout, so it cannot get into a program's own output
The notice one line, once per process however many clients you wrap, before any batch
On exit whatever is still queued is printed on the way out, so a script that finishes before the 2,000 ms interval still shows you its batch
Validation unchanged. An event that section 4 would refuse is dropped before it is queued, so it is not printed either

The printed line cannot drift from the posted body. Each package builds the batch body in exactly one function, and the printing path and the posting path both call it. What you read in inspect mode is the string that would otherwise have gone over the wire, character for character.

With a tap key configured as well, inspect mode still wins, and nothing is sent. Everything is configured, batches print, and the Baseline stays on waiting for your first event for as long as the mode is on. The notice says so when both are set:

Shell
[recovea] tap inspect mode is on AND a tap key is configured: each batch is printed below as the exact JSON body the endpoint would receive, and nothing is sent. No event reaches your Baseline until inspect mode is turned off.

See also

  • Tags: the x-recovea-tags request header, a different tag on a different path.
  • Content-free by schema: the same rule on the gateway's side.