Foxy Audit
Book a demo
← The manual

The wire format

Exactly what leaves your program, field by field. This page is a full list, not an example, because what is not sent is the point of the product.

Skip to the chapter
All chapters

What you'll learn

  • the two requests the SDK makes to your backend;
  • every field in a record, when it appears, and what the backend accepts in it;
  • what is never sent;
  • what the optional desktop signal carries.

The requests

The SDK makes two kinds of request to your backend, both with your API key. It sends records to one address:

POST <your backend URL>/v1/logs/batch
Authorization: Bearer <your API key>
Content-Type: application/json

The body is a JSON list of records, up to ten in one request. Records are grouped by key and backend address, and each goes only with the key it was made with. The request waits at most ten seconds for an answer.

It also reads your workspace's enforcement setting, every five minutes, in the background:

GET <your backend URL>/v1/policies
Authorization: Bearer <your API key>

That request carries no record. Set FOXY_ORG_POLICY=off to stop it, and FOXY_ORG_POLICY_TTL to change how often it runs. See Modes.

Give the base address as your backend URL, such as https://app.foxyaudit.tech. Sending records also works with the full ingest address, because the SDK strips the ending, but the read of your workspace's setting does not. It then fails, and only the debug log says so.

The record

Always present

Field Type What it is
event_id an id made on your machine, one for each call
client_id 32 hex characters this installation's lasting name, made up once for each key and address and kept in the spool, unless you set FOXY_CLIENT_ID
client_seq a whole number a counter for that name, added when the record is saved in the spool
event_type text interaction, stream, blocked, redacted, exception or response_blocked
commitment_alg text how the hashes were made: hmac-sha256, hmac-sha256-salted or sha256-legacy
prompt_hash 64 hex characters the commitment of the prompt
response_hash 64 hex characters the commitment of the answer
token_count a whole number a rough count of tokens, not a bill
policy_tag text the policy tag, folded to lower case
pii_signals a list of labels the kinds of personal data found, sorted, with no repeats. On a call checked before it ran, in block or redact mode, it can also hold prompt_injection or secret_key.

The counter lets the backend notice a gap: if record 41 arrives and then record 43, record 42 is missing or late. The backend then adds a client_seq_gap key to the record it stores.

Present when needed

Field When it appears
agent you passed agent= to the decorator
event_metadata at least one of the keys below applies

The SDK never sends occurred_at, a field the backend also accepts. It sends no time of its own.

Inside event_metadata

Key Set by When
decision the checks a prompt or answer check made a decision
policy_rules the checks rules fired, on the prompt or the answer. Answer rules start with response_.
blocked_reason the checks there is a main reason for the decision
ruleset_version, ruleset_hash the ruleset policy_rules is not empty
policy_tag_raw tag folding your spelling of the tag was changed by folding
system_id your settings you set a system, and it is valid
your own keys you, with metadata= see the warning below

A call in observe mode that nothing flagged, from a function that returns plain text and takes none of the keyword arguments listed below, has no event_metadata at all. That keeps its record the same, field for field, as older versions of the SDK made it. An answer object that carries a model name, an id or token counts gives the record event_metadata, even when nothing was flagged.

When there is no system, system_id is left out, not set to null. The backend checks whether the key is there, and a null would make it refuse the whole request.

Only some metadata keys are sent. The backend accepts keys from a fixed list, and it refuses a whole request that has any other key in it. So the SDK drops a key of yours that is not on the list, with a warning, rather than send it. The metadata the decorator collects by itself is all on that list.

The list is: request_id, trace_id, session_id, provider, model, id, usage, choice_count, tool_names, retrieval_refs, decision, blocked_reason, policy_rules, ruleset_version, ruleset_hash, policy_tag_raw and system_id, and client_seq_gap, which the backend adds itself. Each value, or each item of a list, may be up to 256 characters when written out, and a list may hold up to 64 items. A value over those limits is dropped too, with a warning.

What the backend accepts

The backend checks each field. The values the SDK makes itself always fit. The values you choose are checked before they are sent: agent=, client_id= or FOXY_CLIENT_ID, your own metadata, and a tag or event type you pass to log_interaction.

Field What the backend accepts
policy_tag, event_type lower-case letters, digits and _, up to 32
agent letters, digits, _ . - : / ( ) + and spaces, up to 128
client_id, from FOXY_CLIENT_ID letters, digits and _ . : -, up to 128
event_metadata the keys and limits above

If one value does not fit, the backend refuses the whole request it is in, and every record in it. So the SDK checks them first. agent=, client_id= and a tag or event type given to log_interaction raise ValueError. FOXY_CLIENT_ID is logged as an error and not used. A metadata key or value that does not fit is dropped, with a warning. A token count over 10,000,000 is sent as 10,000,000. A tag you give the decorator is checked for you: see Policies and rules.

What the decorator collects by itself

From your function's keyword arguments, when they are there: request_id, trace_id, session_id, provider, model, tool_names and retrieval_refs. Text is cut to 256 characters, list items to 128, and a list to its first 64 items.

From the answer object, when it has them: id, model, provider, token counts under usage, choice_count, and the names of any tools the model asked to call.

The SDK copies these values as your code passes them, and they are sent and linked into your chain for good. So never put personal or patient details in them, such as a patient's name in a session_id or a record number in retrieval_refs. Use ids that mean nothing outside your own systems.

Worked examples

A call in observe mode that nothing flagged. The hashes and ids here are shortened, and yours will differ:

JSON
{
  "event_id": "<a new id>",
  "client_id": "<32 hex characters>",
  "client_seq": 42,
  "event_type": "interaction",
  "commitment_alg": "hmac-sha256",
  "prompt_hash": "<64 hex characters>",
  "response_hash": "<64 hex characters>",
  "token_count": 137,
  "policy_tag": "hipaa",
  "pii_signals": []
}

A prompt stopped in block mode:

JSON
{
  "event_id": "<a new id>",
  "client_id": "<32 hex characters>",
  "client_seq": 43,
  "event_type": "blocked",
  "commitment_alg": "hmac-sha256",
  "prompt_hash": "<64 hex characters>",
  "response_hash": "<the hash of an empty answer>",
  "token_count": 12,
  "policy_tag": "hipaa",
  "pii_signals": ["ssn_pattern"],
  "agent": "my-model",
  "event_metadata": {
    "decision": "blocked",
    "policy_rules": ["phi.ssn_pattern"],
    "blocked_reason": "phi",
    "ruleset_version": "2026.09.3",
    "ruleset_hash": "<64 hex characters>"
  }
}

The function never ran, so the answer's hash is the hash of an empty answer. The numbers in these examples are only there to show the shape.

What is never sent

  • Your prompt, your model's answer, or any part of either.
  • Where a rule matched, how long the match was, or the matching text.
  • Your commitment key. It is the key in the SDK's key file on your machine, or the one you set with FOXY_COMMITMENT_KEY. Only when the SDK can neither read nor make that file does it use your API key, which goes with every request. It logs a warning when that happens.
  • Your salts. They stay in your own salt file.
  • What the optional deep personal-data check found. Only the kind of thing it found is sent, such as presidio:person.
  • The note the SDK keeps when the backend refused a field. It stays in your spool, because the backend would refuse it too.

The desktop signal

Unless you turn it off, the SDK also sends small messages to the optional desktop app on your own machine, at 127.0.0.1:9999, whether or not the app is running. Each message is under 4,096 bytes of JSON, and a lost one does no harm:

JSON
{"event": "evaluating", "policy": "hipaa", "tokens": 137, "ts": 1790000000.0}

The three messages are:

  • evaluating: a record has been made, after its hashes, and it is about to be saved;
  • hash_ok: the record was saved in the spool, with delivery set to queued. With no API key, nothing is saved, and delivery is local_only;
  • policy_breach: a prompt or answer was stopped. It names up to eight rules.

None of them is a receipt from the backend. Turn them off with FoxyClient(desktop_ping=False).

What comes back

The backend answers each request with receipts, one for each record. The SDK keeps them in the spool. Only log_interaction hands one to your code: with audit_required on, it returns the backend's receipt for its record. A decorated function returns its own value, as it always did.

The backend, not the SDK, links each record into your chain. The SDK sends no chain hash and no time of its own: the backend works both out when the record arrives. The backend also adds keys of its own to the metadata it stores: policy_snapshot and policy_snapshot_hash, your workspace's policy when the record arrived, and client_seq_gap when a record before it is missing or late.

To read your records back, export them: the dashboard's Export page, or GET /v1/logs/export?format=json. That file is what the verifier and foxy explain read.