Skip to content

API reference

The client API in the SDK package.

Sessions, wrapped AI clients and credit metering, on top of the engine. Every output here comes from a recorded run.

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)

Tightlip constructor options
OptionDefaultMeaning
apiKeyTIGHTLIP_API_KEYYour tl_live_ key. Read from the environment variable when not passed.
profile'general'Detection profile, as in the engine.
customTermsnoneAlways protect these: [{ term, type? }].
allowTermsnoneLeave a match alone when its whole text equals one of these.
typesallOnly protect these entity types. Custom terms are always protected.
minConfidence0.5Skip matches scored below this.
failOpenfalseSend prompts when the meter cannot be reached. An empty balance always blocks.
retries2Retries for network errors, HTTP 429 and 5xx from the meter.
retryDelayMs400First retry delay, doubled for each retry.
timeoutMs10000Meter request timeout.
fetchglobal fetchA custom fetch, for tests or other runtimes.
meterUrlthe Tightlip meterMeter 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

TightlipSession members
MemberReturns
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.
vaultThe session vault. Keep it on the device.
Session (Fictional example)
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.'));
Output from the recorded run
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".

Request captured at the stand-in meter
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.

Wrapped OpenAI client
// 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);
What the provider received
[
  { 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].' }
]
What your code received
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.

Errors observed in the recorded run
ErrorWhenMessage observed
TightlipAuthErrorNo key, or the meter answered 401 or 403No Tightlip API key. Set TIGHTLIP_API_KEY or pass { apiKey }. Nothing was sent.
TightlipQuotaErrorThe meter answered 402. Has remaining.Out of Tightlip credits. Nothing was sent. Top up at ...
TightlipMeterErrorThe meter could not be reached or answered another error. Has status.Could not reach the Tightlip meter (HTTP 503). Nothing was sent.
ProtectErrorProtection failed inside the engineTightlip 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.

Handling an empty balance
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;
}