Redactor

Reference

Redactor API

Two verbs, three rails, one credit wallet. Everything below is deterministic — the same request always returns the same body.

Quickstart

Mint a key at /dashboard/api-keys. It is shown once. Every call draws your Ounie credits, and a reserve happens before the run, so a key can be refused but can never overdraw.

curl -s https://redact.ounie.com/api/redact \
  -H "Authorization: Bearer rdc_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Reach Dana at dana@halberd.example, card 4111 1111 1111 1111",
    "mode": "tokenize"
  }'
{
  "ok": true,
  "run": { "id": "…", "chars": 60, "findings": 2, "credits_spent": 1 },
  "mode": "tokenize",
  "redacted": "Reach Dana at [EMAIL_1], card [CREDIT_CARD_1]",
  "mapping": {
    "[EMAIL_1]": "dana@halberd.example",
    "[CREDIT_CARD_1]": "4111 1111 1111 1111"
  },
  "counts": { "email": 1, "credit_card": 1, "phone": 0, "…": 0 },
  "unsure": 0,
  "chars": 60,
  "retention": "Your text, the redacted output and the token mapping exist only for the length of this response…",
  "not_detected": ["Person names", "Postal addresses", "…"]
}

Note Dana is still there. Person names are not detected — see Not detected, which is the section to read before you treat a clean result as an empty one.

Entities

The entities array is optional and defaults to all nine. An unknown value is a hard 400 on every rail, with the offending value named so a caller can fix it rather than retry it.

email

Email address

An RFC-shaped local part, an @, a domain with at least one dot and a 2–24 character TLD. Bounded on both sides so an address inside a longer token is not half-matched.

precision 100%
recall 100%
n = 14
phone

Phone number

Either an E.164 number (a leading + and 8–15 digits) or a North American number written with separators or parentheses, with the area code and exchange code both starting 2–9. A bare run of ten digits with no separators is NOT treated as a phone number.

precision 100%
recall 100%
n = 9
credit_card

Payment card number

A 13–19 digit run that passes the Luhn checksum AND carries a known network prefix (Visa, Mastercard, Amex, Discover, JCB, Diners, UnionPay, Maestro). Luhn alone is reported as unsure, never redacted.

precision 100%
recall 100%
n = 8
iban

IBAN

Two letters, two check digits, then the exact body length that country publishes, validated with the ISO 7064 mod-97 checksum.

precision 100%
recall 100%
n = 8
us_ssn

US Social Security number

AAA-GG-SSSS with a dash or space separator, rejecting the ranges the SSA never issues (area 000/666/900-999, group 00, serial 0000). Also matched when nine bare digits directly follow the words 'SSN' or 'social security'.

precision 100%
recall 100%
n = 5
ip_address

IP address

A dotted quad with every octet in 0–255, or an IPv6 address parsed structurally (never regex-guessed). A quad written directly after a 'v' is treated as a version string and left alone.

precision 100%
recall 100%
n = 9
api_key

API key or token

A credential carrying a published vendor prefix — OpenAI, Anthropic, GitHub, AWS, Google, Slack, Stripe, SendGrid, GitLab, DigitalOcean, Hugging Face, npm, Shopify, Twilio, Notion, Linear. High-entropy strings with no known prefix are reported as unsure, never redacted.

precision 100%
recall 100%
n = 8
jwt

JSON Web Token

Three base64url segments where the first one actually decodes to JSON containing an `alg` field. A string that merely looks like a JWT is not one.

precision 100%
recall 100%
n = 4
private_key

Private key block

A complete PEM block, from a BEGIN … PRIVATE KEY line to its matching END line, including the body.

precision 100%
recall 100%
n = 2

Measured against our own labelled corpus of 26 documents and 67 marked instances, most of which is text that resembles personal data and is not. It is evidence that the detectors work on the cases we could think of, including a set of near-misses that must not fire. It is not an independent benchmark, and it is not a promise about your documents.

corpus: 26 documents · 7,284 characters · 67 labelled instances · measured 2026-08-25

API keys are matched by published vendor prefix: anthropic, aws, digitalocean, github, gitlab, google, huggingface, linear, notion, npm, openai, sendgrid, shopify, slack, stripe, twilio. A high-entropy string with no known prefix is reported in unsure and left alone — a hash, a content id and a credential are the same shape.

Not detected

This list is API surface, not a footnote. An agent that does not know a category is missing will read a clean output as “there was nothing there” — the one lie a redactor must never tell. It is also returned in every redact response and by the MCP tool get_supported_entities.

not detectedPerson names

There is no rule that separates a person's name from any other pair of capitalised words, and this engine deliberately runs no model. A name-detector built on a list would miss every name not on the list while looking like it worked. Names stay visible in the output — see the unsure count and the hero on the homepage.

not detectedPostal addresses

Street addresses vary by country in ways no regex settles, and the plausible patterns collide with ordinary prose. A partial address redaction is worse than none: it removes the street and leaves the city, so the record still identifies one household.

not detectedDates of birth

A date is only a birth date because of what surrounds it. Redacting every date would gut a document; redacting some of them would be a guess about which.

not detectedPassport and national ID numbers

Outside the US SSN there is no checksum we can rely on across issuing countries, and the formats overlap with ordinary reference numbers.

not detectedBare account and routing numbers

US ABA routing numbers do carry a checksum, but nine bare digits with no label match far too many internal identifiers. Nine-digit runs in the SSA's issued range are reported in `unsure` rather than removed.

not detectedGeneric high-entropy secrets

A random-looking string is as likely to be a hash, a content id or a base64 payload as a credential. Only credentials with a published vendor prefix are redacted; the rest are counted in `unsure` so you can look.

not detectedAnything inside an image, a PDF or an audio file

This app takes text. It does no OCR and no transcription — use Extractor or Transcriber first, then send the text here.

Modes and tokens

mode is mask or tokenize, defaulting to tokenize.

  • tokenize — each distinct value gets a stable numbered token, shared across every document in the call, and a mapping is returned. Feed that mapping to /api/rehydrate to get the original back.
  • mask — a fixed per-type marker, and no mapping at all. Irreversible on purpose.

token_format is one of [EMAIL_1] · <EMAIL_1> · {{EMAIL_1}}. All three round-trip.

If your input already contains a placeholder in this shape, new tokens are numbered above it and the collision is reported in preexisting_tokens. Rehydrating will not restore the ones that were already there — they were never ours.

curl -s https://redact.ounie.com/api/rehydrate \
  -H "Authorization: Bearer rdc_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Reach Dana at [EMAIL_1]",
    "mapping": { "[EMAIL_1]": "dana@halberd.example" }
  }'

Rehydrate returns unresolved_tokens (placeholders still in the text with no mapping entry) and unused_tokens (mapping entries that never appeared). Neither is an error; both are things you would want to know.

REST

POST/api/redactRedact. Priced from the character count.bills
GET/api/redactYour run history. Metadata only.free
GET/api/redact/{id}One run's metadata.free
POST/api/rehydrateRestore from your mapping.bills
GET/api/pricingPublic: prices, entities, refusals.free
GET/api/creditsYour spendable balance.free

Auth is your Ounie session cookie or a Bearer key. Hosts that cannot set headers may pass ?api_key=. Your ounie.com master key (ounie_live_…) works too, if you have enabled it in your Ounie settings.

Caps: 200,000 characters and 50 documents per call, 5,000 mapping entries, 5 runs in flight per account.

MCP

Streamable HTTP at /api/mcp, legacy SSE at /api/sse. Claude, Cursor, ChatGPT and the Ounie AI Team all connect. The AI Team cannot set static headers, which is why the key may ride in the URL.

{
  "mcpServers": {
    "redactor": {
      "url": "https://redact.ounie.com/api/mcp?api_key=rdc_live_..."
    }
  }
}
redact_textRedact one or many documents.bills
rehydrate_textRestore from a mapping you hold.bills
get_supported_entitiesWhat is detected, and what is not.public
get_runOne past run's metadata.free
list_runsYour run history.free
get_pricingPrices and the x402 endpoints.public
get_credit_balanceSpendable credits.free
whoamiThe key's owner.free

Insufficient credits come back as an insufficient_credits tool error naming the exact shortfall and where to top up. A tool call can be refused; it can never overdraw.

x402

Keyless pay-per-call in USDC on Base, for agents with no Ounie account.

POST https://redact.ounie.com/api/x402/redact
POST https://redact.ounie.com/api/x402/rehydrate

1. POST your payload with no X-Payment header
2. 402 comes back with terms quoting THIS payload
3. sign the EIP-3009 authorization
4. retry with X-Payment: <base64 payload>

The price is computed from the character count of what you sent — from $0.012 per block of 10,000 characters. There is no flat rate baked into the route, because the exact scheme compares the signed value against maxAmountRequired, and a route that quoted one price and wanted another would simply be refused after doing the work.

Gate 1 — refused free, before any quote

An unsupported entity, an unknown mode or token format, an empty body, an over-cap payload, a malformed mapping: all knowable from the request alone and all always fatal. They return a 4xx with no price in the body, so you never build and sign an authorization for a call that cannot succeed.

Verify, run, then settle

The signature is verified off-chain first, the work runs, and the money moves last. An x402 settlement is final — there is no refund on this rail the way there is on the credit rail — so the ordering is the only protection a payer gets, and it is not negotiable.

Errors

400text_requiredNo text or texts, or all of them empty.
400text_or_texts_not_bothSend one or the other.
400too_many_textsMore than 50 documents.
400text_too_longMore than 200,000 characters.
400unsupported_entity:<value>Not in the enum. The value is named.
400invalid_modeNot one of mask, tokenize.
400invalid_token_formatNot one of bracket, angle, curly.
400mapping_requiredRehydrate needs a non-empty mapping.
400mapping_too_largeOver 5,000 entries.
401unauthorizedNo session and no valid Bearer key.
402insufficient_creditsCarries required, balance and where to top up.
429too_many_runningFive runs already in flight.
502redact_failedThe run threw. Nothing was charged.

Retention

Your text, the redacted output and the token mapping exist for the length of the response and no longer. They are not written to disk, to the database, or to a log — error paths record an identifier rather than a message, because a message can quote its input.

What is stored is a row of counts per run: characters, findings by type, unsure, credits, timestamps. That is what makes your history and your invoice legible, and it cannot reconstruct anything you sent. There is a test in the repository that reads the database migration and fails if a column appears that could hold any of it.

The practical consequence: if you lose a mapping, the values are gone. We cannot recover them because we never had them. That is not a gap we intend to close — it is why handing you the mapping is safe.

Pricing

1 Ounie credit per 10,000 characters, rounded up, per call. A credit is one cent. Reads are free, runs bill, and a failed run refunds pool for pool.

1,000 chars1 cr$0.012 USDC
10,000 chars1 cr$0.012 USDC
10,001 chars2 cr$0.024 USDC
50,000 chars5 cr$0.060 USDC
200,000 chars20 cr$0.240 USDC

9 detectors, one price — the entity list you pick does not change what a call costs, because the work is dominated by walking the text rather than by any one rule.