Zero-width character encoder
Hides a secret inside ordinary text with invisible Unicode characters: “Hi” tucked into “Hello world” becomes 18 zero-width characters nobody sees, and decoding reveals it again. Handy for watermarks and lessons, but it is not encryption. Free in your browser.
- Free
- Runs in your browser
- No sign-up
- Your data stays on your device
Run — free
Worked example
- Secret
- Hi
- Cover
- Hello world
- Calculation mode
- encode
- Result
- Hello world
- Encoding
- zero_width
- Secret length
- 2
- Cover length
- 11
Zero-width characters are Unicode code points that occupy no visible width, so ordinary readers never notice them while parsers and copy-paste still carry the bytes. Decode scans for the same frame, rebuilds bits into bytes, and returns the original secret.
How a secret becomes invisible
Zero-width characters are Unicode code points that take up no visible space, so readers never notice them while copy and paste still carries them. The tool turns your secret into UTF-8 bytes and walks every bit from the most significant down: a zero becomes U+200B, the zero width space, and a one becomes U+200C, the zero width non-joiner. A start marker, U+200D, and an end marker, U+FEFF, frame the bits, and the whole invisible block goes right after the first character of your cover text. Hiding “Hi” in “Hello world” uses 2 bytes, 16 bits and 18 invisible characters.
What it is good for, and what it is not
It suits classroom steganography demos, privacy workshops and light watermarks, such as tucking a team or ticket code into a draft so a leaked paste can be traced without changing the visible layout. It is not encryption: anyone who knows the scheme can pull the secret out, so do not rely on it to protect anything that matters. Decoding looks only between the first start marker and the next end marker, so other invisible characters elsewhere in a document are ignored. A secret can be up to 4096 characters long.
Where a zero-width encoder earns its keep in products and teaching
Security and privacy workshops use a zero-width character encoder so learners can paste stego text into a chat or document, confirm the message still looks clean, then decode it in a second step and see the bit map spelled out in the formula field. Because the implementation is pure parse with no model tokens and no outbound network, latency stays in the analytic floor and cost stays predictable.
Prefer this tool when the job is hide-and-reveal with Unicode format characters; choose real cryptography libraries when confidentiality under attack is the requirement. Document for end users that some platforms normalize or strip zero-width characters on send, so always round-trip through decode after the actual channel you care about.
What you can do with it
Classroom steganography demos
Show students how invisible Unicode bits hide a short secret inside ordinary cover sentences, then reveal it with decode on the same deterministic module.
Copy-leak watermarks for drafts
Embed a team or ticket id as zero-width bits in sample marketing text so leaked pastes can be attributed without changing visible layout.
FAQ
How do I hide a secret with zero-width characters?
Call with mode encode (or omit mode), pass non-empty secret and cover.
How do I reveal a hidden message?
Call with mode decode and pass the stego string as text.
Which Unicode characters are used?
Bit 0 is U+200B, bit 1 is U+200C, the start marker is U+200D, and the end marker is U+FEFF.
Is this encryption?
No. Zero-width encoding only hides bits from casual readers. Use cryptography when you need confidentiality.
Will the hidden message survive copy and paste?
Usually, because the invisible characters travel with the text. Some apps and sites normalize or strip zero-width characters when you send, though, so after passing the text through the channel you care about, decode it once to confirm the secret is still there.
Related tools
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/enc/zero-width \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"mode":"encode","op":"encode"}'const res = await fetch("https://api.kit.forhosting.com/enc/zero-width", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"mode": "encode",
"op": "encode"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/enc/zero-width",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"mode": "encode",
"op": "encode"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/enc/zero-width", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"mode":"encode","op":"encode"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"mode":"encode","op":"encode"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/enc/zero-width", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"mode": "encode",
"op": "encode"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "enc.zero_width",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
Technical notes
This zero-width character encoder turns a secret UTF-8 string into a stream of U+200B and U+200C symbols framed by U+200D and U+FEFF markers, then inserts that invisible payload into cover text after the first character. Invalid markers, incomplete bit groups, or missing fields fail with invalid_input and are not billed. The tool is not encryption—anyone who knows the scheme can extract the payload—so treat it as a teaching aid, light watermark, or demo layer rather than a secrecy boundary.
How zero-width bit encoding hides a secret in plain sight
The encoder first converts the secret to UTF-8 bytes, then walks every bit from most significant to least significant within each byte. Bit zero becomes U+200B (ZERO WIDTH SPACE) and bit one becomes U+200C (ZERO WIDTH NON-JOINER). Because every step is pure string and byte arithmetic, the same secret and cover always produce the same stego text on every runtime. Visible cover text stays readable because the payload uses only format characters that render with zero advance width. A start marker U+200D and an end marker U+FEFF wrap that bit stream so decode can ignore unrelated invisible characters elsewhere in a document and still recover a well-defined payload. Decode reverses the path: locate the first start marker, the next end marker, accept only U+200B and U+200C between them, require the bit count to be a multiple of eight, reassemble bytes, and UTF-8 decode with a fatal decoder so corrupt streams surface as invalid_input instead of mojibake. That contract matches classroom explanations of steganography without pretending the channel is confidential: zero-width hiding is about invisibility in the UI, not resistance to a motivated reader who inspects code points.
Input contract, modes, response shape, and errors
Send mode encode or decode. Encode requires a non-empty secret (aliases message or payload, max 4096 characters) and non-empty cover text (also accepted as text, input, value, or data, max 100000 characters). Decode requires non-empty text containing a framed payload (aliases cover, input, value, data, or stego, max 200000 characters). Mode aliases include hide and embed for encode, and reveal or extract for decode; the default is encode when mode is omitted. On successful encode the body includes result (stego string), mode, encoding zero_width, secret_length, cover_length, result_length, byte_count, bit_count, zero_width_count, formula, and a short message summarizing how many UTF-8 bytes were hidden. On successful decode the body includes result and secret (the revealed string), mode, encoding, lengths, bit and byte counts, zero_width_count, formula, and a reveal message. Missing fields, empty strings, unknown mode, missing start or end markers, non bit characters inside the frame, nested markers, a bit length that is not a multiple of eight, or invalid UTF-8 after bit assembly all throw invalid_input with a clear English message. Those failures do not consume prepaid balance. Keep payloads modest for widgets and pin result strings in tests—zero-width characters are real Unicode, so JSON examples must preserve them exactly.
Where a zero-width encoder earns its keep in products and teaching
QA teams inject invisible watermarks into sample marketing copy to detect when drafts leak through uncontrolled paste paths; a deterministic API call at $0.002 per successful item is cheaper than standing up a custom Worker for a one-off script.
Integration use cases
CI checks for paste pipelines
Encode a known secret into fixtures, push them through your editor or chat integration, and decode to verify zero-width characters survived the path.
Technical questions
What does the zero-width character encoder cost?
The browser tool is free. API requests use prepaid KIT balance at $0.002 per successful call; invalid input is not billed.