Reference
Two verbs, three rails, one credit wallet. Everything below is deterministic — the same request always returns the same body.
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.
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 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.
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.
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.
IBAN
Two letters, two check digits, then the exact body length that country publishes, validated with the ISO 7064 mod-97 checksum.
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'.
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.
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.
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.
Private key block
A complete PEM block, from a BEGIN … PRIVATE KEY line to its matching END line, including the body.
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.
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.
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.
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.
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.
Outside the US SSN there is no checksum we can rely on across issuing countries, and the formats overlap with ordinary reference 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.
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.
This app takes text. It does no OCR and no transcription — use Extractor or Transcriber first, then send the text here.
mode is mask or tokenize, defaulting to tokenize.
mapping is returned. Feed that mapping to /api/rehydrate to get the original back.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.
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.
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_..."
}
}
}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.
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.
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.
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.
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.