Current availability
The tightlip package 0.1.0 from the download page contains this API. We ran it on 15 October 2026 against local stand-ins for the credit meter and the AI provider, so no request left the test machine. The outputs below come from that run. The stand-in meter answered with 399 credits remaining.
There is no separate Hushbridge HTTP API today. The only web endpoint the SDK calls is the Tightlip credit meter, which the current Tightlip documentation describes.
Package exports
Tightlip, TightlipSession, TightlipError, TightlipQuotaError, TightlipAuthError and TightlipMeterError, plus every engine export: protect, detect, restore, createStreamRestorer, registerDetector, estimateCredits, Vault, ProtectError and PLACEHOLDER_INSTRUCTION. The local proxy is a separate entry point, tightlip/proxy.
new Tightlip(options)
| Option | Default | Meaning |
|---|---|---|
apiKey | TIGHTLIP_API_KEY | Your tl_live_ key. Read from the environment variable when not passed. |
profile | 'general' | Detection profile, as in the engine. |
customTerms | none | Always protect these: [{ term, type? }]. |
allowTerms | none | Leave a match alone when its whole text equals one of these. |
types | all | Only protect these entity types. Custom terms are always protected. |
minConfidence | 0.5 | Skip matches scored below this. |
failOpen | false | Send prompts when the meter cannot be reached. An empty balance always blocks. |
retries | 2 | Retries for network errors, HTTP 429 and 5xx from the meter. |
retryDelayMs | 400 | First retry delay, doubled for each retry. |
timeoutMs | 10000 | Meter request timeout. |
fetch | global fetch | A custom fetch, for tests or other runtimes. |
meterUrl | the Tightlip meter | Meter address. We set this to a local stand-in for the run on this page. |
tl.session(vault?) returns a TightlipSession: one vault for one conversation. Pass a saved vault to resume one.
Sessions
| Member | Returns |
|---|---|
protect(text, overrides?) | A promise of { text, detections, counts, credits, remaining }. Charges credits only when something was replaced. |
protectAll(texts, overrides?) | A promise of { texts, replaced, credits, remaining }. Meters all texts as one prompt, such as every message in one chat request. |
restore(text) | The text with real values back. Local only. |
streamRestorer() | push(chunk) and flush(), as in the engine. |
vault | The session vault. Keep it on the device. |
import { Tightlip } from 'tightlip';
const tl = new Tightlip({ apiKey: process.env.TIGHTLIP_API_KEY, profile: 'legal' });
const s = tl.session(); // one vault per conversation
const safe = await s.protect('Draft a reply to our client Rosalind Achterberg about Case No. 2:25-cv-01482.');
console.log(safe.text);
console.log(safe.counts, safe.credits, safe.remaining);
const nothing = await s.protect('Summarise the attached hearing schedule in three bullet points.');
console.log(nothing.credits);
const batch = await s.protectAll(['Our client Rosalind Achterberg called.', 'Her email is rosalind.a@example.com.']);
console.log(batch.texts, batch.replaced, batch.credits);
console.log(s.restore('Dear [PERSON_1], your hearing for [CASE_NUMBER_1] is set.'));Draft a reply to our client [PERSON_1] about Case No. [CASE_NUMBER_1].
{ PERSON: 1, CASE_NUMBER: 1 } 1 399
0
[ 'Our client [PERSON_1] called.', 'Her email is [EMAIL_1].' ] 2 1
Dear Rosalind Achterberg, your hearing for 2:25-cv-01482 is set.What the meter receives
When a call replaced something, the SDK sends one request to the meter. We captured it at the stand-in. It carries your key and a credit count. It never carries your text, the placeholders, the detections or the vault. The proxy sends the same request with "source":"proxy".
POST <meter URL>
authorization: Bearer tl_live_EXAMPLEKEY0000
content-type: application/json
{"credits":1,"source":"sdk"}Credits follow the engine's estimateCredits(): one per started block of 32,000 characters, at USD 0.05 per credit. For protectAll() the texts are counted together.
Wrapped OpenAI and Anthropic clients
tl.wrapOpenAI(client, options?) protects chat.completions.create and responses.create. tl.wrapAnthropic(client, options?) protects messages.create and messages.stream. Every message is protected and every answer is restored before your code sees it. When something was replaced, PLACEHOLDER_INSTRUCTION is added to the system prompt. Everything else on the client passes through.
Each call uses a fresh vault. To keep numbering stable across calls, pass { session: tl.session() } as the options.
// openaiClient is your OpenAI client. Any object with the same method shape works.
const openai = tl.wrapOpenAI(openaiClient);
const res = await openai.chat.completions.create({
model: 'your-model',
messages: [{ role: 'user', content: 'Write to our client Rosalind Achterberg about Case No. 2:25-cv-01482.' }],
});
console.log(res.choices[0].message.content);[
{ role: 'system', content: 'Some details in this conversation were replaced with placeho...' },
{ role: 'user', content: 'Write to our client [PERSON_1] about Case No. [CASE_NUMBER_1].' }
]Dear Rosalind Achterberg, your hearing for 2:25-cv-01482 is set.Our run used stand-in clients with the same method shapes, not the provider SDKs. It covered chat.completions.create, responses.create and messages.create. For the Anthropic call, the instruction was placed before the existing system prompt, and the answer came back as "I will write to Rosalind Achterberg today.". Streamed answers were checked through the local proxy.
Errors
Every SDK error means the prompt was not sent. All four classes extend TightlipError.
| Error | When | Message observed |
|---|---|---|
TightlipAuthError | No key, or the meter answered 401 or 403 | No Tightlip API key. Set TIGHTLIP_API_KEY or pass { apiKey }. Nothing was sent. |
TightlipQuotaError | The meter answered 402. Has remaining. | Out of Tightlip credits. Nothing was sent. Top up at ... |
TightlipMeterError | The meter could not be reached or answered another error. Has status. | Could not reach the Tightlip meter (HTTP 503). Nothing was sent. |
ProtectError | Protection failed inside the engine | Tightlip could not protect this text, so it must not be sent: ... |
With failOpen: true and the stand-in meter answering 503, the call returned the placeholder text with 1 credit counted and no balance. An empty balance still throws.
import { TightlipQuotaError } from 'tightlip';
try {
const safe = await s.protect(text);
} catch (err) {
if (err instanceof TightlipQuotaError) {
// Out of credits. Nothing was sent.
}
throw err;
}