Articles Integrations

How to Add an “Open in Inscrive” Button to Your Website

One click takes your readers from a LaTeX template or snippet on your site into a new inscrive project. Already have “Open in Overleaf”? Change one host and you are done.

inscrive.io · Sep 14, 2026 · 12 min read
How to Add an “Open in Inscrive” Button to Your Website

If you publish LaTeX (a template gallery, a tutorial site, a university page with the official thesis class), your readers all hit the same wall: they have to download a zip, unpack it, and find somewhere to compile it. An “Open in Inscrive” button removes that step. One click, and the template or snippet on your page becomes a new inscrive project that they can edit and compile straight away in the browser, with no TeX installation. This guide shows you how to add one, with copy-paste HTML for every common setup.

The short version: inscrive’s https://app.inscrive.io/docs endpoint mirrors Overleaf’s /docs endpoint. The parameter names are the same, it needs no sign-in on your side, and there is no API key to request and no partner agreement to sign. If your site already has an “Open in Overleaf” button, you are one line away.

Already have an “Open in Overleaf” button?

Change the host. Everything else stays as it is.

- <form action="https://www.overleaf.com/docs" method="post" target="_blank">
+ <form action="https://app.inscrive.io/docs" method="post" target="_blank">

The same goes for plain links:

- https://www.overleaf.com/docs?snip_uri=https://templates.example.org/thesis.zip
+ https://app.inscrive.io/docs?snip_uri=https://templates.example.org/thesis.zip

snip_uri, snip_uri[], snip_name[], snip, encoded_snip, encoded_snip[], engine and main_document all keep their names and meaning. A few Overleaf-specific options, such as visual_editor, are accepted but have no effect; they are listed under “Differences from Overleaf” below. If you want to offer both editors side by side, see the “One form, two buttons” recipe below.

Quick start

A plain link

The simplest integration is a link to a zip of your template. URL-encode the value of snip_uri (in JavaScript, encodeURIComponent) so that any ? or & in your file’s address survives.

<a
	href="https://app.inscrive.io/docs?snip_uri=https%3A%2F%2Ftemplates.example.org%2Fthesis.zip"
	target="_blank"
	rel="noopener"
>
	Open in Inscrive
</a>

The same thing as a form

A form sends the same parameters with POST. It is easier to read, needs no URL-encoding for snip_uri, and is the only sensible choice once you send LaTeX source rather than a link to it.

<form action="https://app.inscrive.io/docs" method="post" target="_blank">
	<input type="hidden" name="snip_uri" value="https://templates.example.org/thesis.zip" />
	<button type="submit">Open in Inscrive</button>
</form>

Both are ordinary top-level navigations, so there is no CORS to configure. Your visitor has to land on inscrive’s confirm card, so send them there with a link or a form. A background fetch(), beacon or image request to the endpoint is refused with a 403.

Recipes

Every recipe below is complete: paste it into a page and it works. Replace the example.org addresses with your own.

A zip template, with a title and a main document

Most galleries host each template as a zip. A GitHub, Gitea or Codeberg archive link works directly, and the repository name becomes the project title unless you set one. The single top-level folder that archives wrap around their contents is stripped for you.

<form action="https://app.inscrive.io/docs" method="post" target="_blank">
	<input
		type="hidden"
		name="snip_uri"
		value="https://github.com/example-lab/thesis-template/archive/refs/heads/main.zip"
	/>
	<input type="hidden" name="snip_name" value="Example Lab Thesis" />
	<input type="hidden" name="main_document" value="thesis.tex" />
	<input type="hidden" name="engine" value="lualatex" />
	<button type="submit">Open in Inscrive</button>
</form>

For a zip, snip_name is only used as the project title. main_document names the root .tex file. You can usually leave it out, because inscrive detects the root document on its own, but set it when a template contains several files with a \documentclass.

If you generate links for a whole gallery, a small helper keeps the encoding right:

function inscriveLink(zipUrl, title) {
	const snipUri = encodeURIComponent(zipUrl);
	const snipName = encodeURIComponent(title);
	return `https://app.inscrive.io/docs?snip_uri=${snipUri}&snip_name=${snipName}`;
}

// https://app.inscrive.io/docs?snip_uri=https%3A%2F%2Ftemplates.example.org%2Fthesis.zip&snip_name=Example%20Lab%20Thesis
inscriveLink('https://templates.example.org/thesis.zip', 'Example Lab Thesis');

When you write such a link by hand inside an HTML attribute, spell each & as &.

A single .tex file by URL

Point snip_uri at the file. Without a snip_name, the file keeps the last part of its URL, here hello-world.tex, and the project is titled hello-world.

<a
	href="https://app.inscrive.io/docs?snip_uri=https%3A%2F%2Ftemplates.example.org%2Fexamples%2Fhello-world.tex"
	target="_blank"
	rel="noopener"
>
	Open in Inscrive
</a>

If the file has no \documentclass, it is wrapped in a small standard document so it compiles (see “Decoration” in the parameter reference below).

A raw snippet from a textarea

The snip parameter takes LaTeX source as-is, with no encoding. A <textarea> is the easiest way to post it, and you can add the hidden attribute if your page already shows the code elsewhere. Because the source sits inside your HTML, escape it as HTML: write every & as & and every < as <.

<form action="https://app.inscrive.io/docs" method="post" target="_blank">
	<textarea name="snip" rows="10" cols="60">
\documentclass{article}
\usepackage{amsmath}
\begin{document}
\begin{align*}
  f(x) &amp;= (x + 1)^2 \\
       &amp;= x^2 + 2x + 1
\end{align*}
\end{document}</textarea>
	<button type="submit">Open in Inscrive</button>
</form>

An “Open” link on every code box

On a tutorial site the code is already on the page. One hidden form and a few lines of JavaScript turn every code box into a button. The script reads the code, encodes it with encodeURIComponent (not the old escape function, which breaks non-ASCII characters) and posts it as encoded_snip.

<form id="inscrive-form" action="https://app.inscrive.io/docs" method="post" target="_blank">
	<input id="inscrive-snip" type="hidden" name="encoded_snip" />
</form>

<figure class="code-box">
	<pre><code class="language-latex">\documentclass{article}
\begin{document}
Hello, world!
\end{document}</code></pre>
	<button type="button" class="open-in-inscrive">Open in Inscrive</button>
</figure>

<script>
	function openInInscrive(codeElement) {
		document.getElementById('inscrive-snip').value = encodeURIComponent(codeElement.textContent);
		document.getElementById('inscrive-form').submit();
	}

	document.querySelectorAll('.open-in-inscrive').forEach((button) => {
		button.addEventListener('click', () => {
			openInInscrive(button.closest('.code-box').querySelector('code'));
		});
	});
</script>

textContent keeps line breaks that are real newline characters, which is what most syntax highlighters produce. If yours renders line breaks as <br> elements, use innerText instead.

Several files in one project

Send one encoded_snip[] per file, each followed by the matching snip_name[]. Names may include folders. Multi-file requests are never wrapped in a standard document, so a chapter file stays a chapter file.

<button type="button" id="open-book">Open in Inscrive</button>

<script>
	function openFilesInInscrive(files, mainDocument) {
		const form = document.createElement('form');
		form.action = 'https://app.inscrive.io/docs';
		form.method = 'post';
		form.target = '_blank';
		form.hidden = true;

		const addField = (name, value) => {
			const input = document.createElement('input');
			input.type = 'hidden';
			input.name = name;
			input.value = value;
			form.append(input);
		};

		for (const [fileName, source] of Object.entries(files)) {
			addField('encoded_snip[]', encodeURIComponent(source));
			addField('snip_name[]', fileName);
		}
		addField('main_document', mainDocument);

		document.body.append(form);
		form.submit();
	}

	document.getElementById('open-book').addEventListener('click', () => {
		openFilesInInscrive(
			{
				'main.tex': String.raw`\documentclass{book}
\begin{document}
\include{chapters/introduction}
\end{document}`,
				'chapters/introduction.tex': String.raw`\chapter{Introduction}
This chapter lives in its own file.`
			},
			'main.tex'
		);
	});
</script>

Static pages can do the same without JavaScript: one <textarea name="snip[]"> per file, each followed by an <input type="hidden" name="snip_name[]">. Use one source family per request, though. Mixing snip_uri, snip and encoded_snip in the same request is rejected, because it would be unclear which name belongs to which file.

A base64 data URL

snip_uri also accepts a data: URL. It is handy when you want to embed a file directly in the page instead of hosting it.

<form action="https://app.inscrive.io/docs" method="post" target="_blank">
	<input
		type="hidden"
		name="snip_uri"
		value="data:application/x-tex;base64,XGRvY3VtZW50Y2xhc3N7YXJ0aWNsZX0KXGJlZ2lue2RvY3VtZW50fQpIZWxsbyBmcm9tIGEgZGF0YSBVUkwhClxlbmR7ZG9jdW1lbnR9Cg=="
	/>
	<input type="hidden" name="snip_name" value="hello.tex" />
	<button type="submit">Open in Inscrive</button>
</form>

A zip works the same way, as data:application/zip;base64,…. Zips are recognised by their content, not by the MIME type. To produce the base64 string on a server or in a build step:

base64 < hello.tex | tr -d '\n'

A posted form is limited to 512 KB, and base64 makes a file about a third larger. Keep data URLs to files of a few hundred kilobytes, and for anything larger, host a zip and link it.

One form, two buttons

You do not have to choose between editors. HTML’s formaction attribute lets a submit button override the form’s action, so one form with one set of fields can serve both buttons. The first button posts to the form’s action, the second to its own formaction.

<form action="https://www.overleaf.com/docs" method="post" target="_blank">
	<input type="hidden" name="snip_uri" value="https://templates.example.org/thesis.zip" />
	<input type="hidden" name="main_document" value="thesis.tex" />

	<button type="submit">Open in Overleaf</button>
	<button type="submit" formaction="https://app.inscrive.io/docs">Open in Inscrive</button>
</form>

Any of the form recipes on this page can be written this way, because both endpoints read the same parameter names. Test the Overleaf button as well, though: some conveniences described here, such as the latex engine value and the lenient matching of main_document, are inscrive behaviour that Overleaf does not document.

Sites that use runlatex.js

Many LaTeX tutorial sites, including learnlatex.org, add their example buttons with David Carlisle’s runlatex.js. It keeps the Overleaf address and the button label in settings you can override. Set them in a script that runs right after runlatex.js loads, because the buttons are built when the page finishes loading:

<script src="runlatex.js"></script>
<script>
	runlatex.overleafURI = 'https://app.inscrive.io/docs';
	runlatex.texts['Open in Overleaf'] = 'Open in Inscrive';
</script>

Be aware of what this does. runlatex has a single Overleaf slot, so these two lines point that button at inscrive instead of Overleaf; they do not add a second button. The TeXLive.net button is unaffected. runlatex posts encoded_snip[], snip_name[], engine and sometimes main_document, all of which inscrive reads. It names every example document.tex, so the imported projects are titled “document”. Engine names that inscrive does not recognise (runlatex can send context, for example) fall back to automatic engine detection, so those examples open instead of failing. For ConTeXt and plain TeX examples runlatex also adds its own latexmkrc file, so test one of those before you rely on it.

Parameter reference

The endpoint is https://app.inscrive.io/docs. It accepts GET with a query string, and POST as application/x-www-form-urlencoded (a normal HTML form) or multipart/form-data. On a POST, parameters in the form’s action query string are added after the form fields. The older address for GET links, https://app.inscrive.io/import?snip_uri=…, keeps working with the same parameters.

ParameterWhat it does
snip_uri, snip_uri[]URL of a file to import: http:// or https:// on a public host and the standard port (up to 5 redirects, each of which must be the same), or a data: URL, base64 or percent-encoded. A zip is unpacked into the project root; anything else becomes one file.
zip_uriOlder spelling of snip_uri, still accepted. Ignored when snip_uri is also present.
snip, snip[]Raw LaTeX source, no encoding.
encoded_snip, encoded_snip[]URL-encoded LaTeX source, from encodeURIComponent or PHP’s urlencode (a + is read as a space). Invalid percent-encoding is rejected.
snip_name, snip_name[]File name for the source in the same position, folders allowed (figures/plot.png). Percent-encoded names are decoded. With exactly one source it is also the project title, minus the extension. For a zip, or a single source whose name has no file extension (snip_name=Example Lab Thesis), it is used only as the title.
main_documentPath of the root .tex file. It must exist in the project, or the import stops with an error naming it. Matching is case-insensitive and tolerates an archive’s wrapper folder. Without it, the root is detected from % !TEX root, \documentclass and \begin{document}.
enginepdflatex, xelatex, lualatex, latex_dvipdf or latex (the last two both compile with LaTeX to a PDF). A -dev suffix is read as the base engine. Left out or unrecognised: the engine is detected from the preamble.
visual_editor, rich_text, commentAccepted and ignored.

Array spellings. Every array parameter accepts name[]=a&name[]=b, a repeated name=a&name=b, and an indexed name[0]=a&name[1]=b (ordered by index). A single name=a counts as a one-element array. Unknown parameters are ignored, and an empty field is treated as not sent.

One source family per request. Use snip_uri (or zip_uri), snip, or encoded_snip, not a mix.

Default file names. A URL source without a name keeps the last segment of its URL when that has a file extension. Otherwise the first source is main.tex and later ones are file-2.tex, file-3.tex and so on. Two sources that end up at the same path are rejected.

Decoration. When a request has exactly one source, the source is not a zip, its name ends in .tex, and it contains no \documentclass, it is wrapped in the same template Overleaf uses:

\documentclass[12pt]{article}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{tikz}
\begin{document}
SNIPPET
\end{document}

Project title. In order of preference: if the request has exactly one source and it has a name, that name without its extension; for a single URL source without a name, the repository name of a GitHub, Gitea or Codeberg archive link, or else the file name without .zip; otherwise the main_document name without its extension, then the first source’s name, then “Imported project”. Control characters are removed and titles are capped at 200 characters.

Encoding. Text is read as UTF-8. Line endings are normalised to Unix newlines.

What your visitor experiences

  1. They click your button and land on an “Import into Inscrive” card on app.inscrive.io. It shows the project title and, when known, the site the template came from.
  2. If they are signed in, they press “Import this project”.
  3. If they are not, they press “Create an account and import”, register or sign in, verify their email, and come back to the same card. The request is held for 24 hours, so the detour through their inbox does not lose your template.
  4. The project opens in the editor with the main document selected, ready to compile. If you sent no main_document and the project holds several documents that could be the root, the visitor is asked to pick one.

Nothing is created until the visitor presses Import, so a stray click on your site never leaves an empty project behind. An imported project counts against the visitor’s plan like any other project they create. If an import fails, the visitor can retry it; once it succeeds, the link on the card is used up.

Where it lands matters to some of your readers, especially at European universities. inscrive stores all data on EU soil, on Hetzner infrastructure in Germany and Finland, in ISO 27001-certified data centres, with no third-country transfers. The background is in why your LaTeX editor should be hosted in the EU and how to choose a GDPR-compliant LaTeX editor.

Limits

These are generous for a template gallery, but worth knowing before you ship.

LimitValue
Sources per request50 (and at most 1000 form fields)
POST body512 KB, measured as sent (encoded)
Stored request (sources, names and inline content)1 MB per request, 5 MB per hour per client address
Downloaded content (all http(s) sources together)100 MB, and 60 seconds of total download time
Zips2000 files, 250 MB unpacked, no password-protected entries
Import links120 per hour per client address
Completed imports10 per hour per account, 60 per client address
Held request24 hours, used up when the import succeeds

Two practical consequences. First, the 512 KB applies to the encoded body, and LaTeX is expensive to encode: each backslash becomes %5C through encodeURIComponent, and a normal form encodes the % again. A large encoded_snip can grow three to five times on the way. Second, keep snippets out of GET links entirely, because servers cap URL length at around 8 to 16 KB. Use a form, and for anything big, host a zip and link it with snip_uri.

Troubleshooting

A 400 page naming a parameter. The error names what is wrong. The usual causes:

  • Two source families in one request, for example snip_uri and encoded_snip together. Pick one.
  • encoded_snip that was never encoded. A LaTeX comment starts with %, which is not valid percent-encoding. Run the source through encodeURIComponent, or send it raw as snip instead.
  • Two files that resolve to the same path. Give each source its own snip_name.

An error naming your main_document. The path must match a file that ends up in the project. The file list is only known once the files are fetched, so this error appears when the visitor presses Import, not when they click your button: test your integration all the way through an import. Check that the zip really contains the file (the wrapper folder of a GitHub archive does not matter, and neither does case), or that it matches one of your snip_name values. If you are unsure, leave main_document out and let inscrive detect the root file.

A URL is refused. http and https sources must resolve to public addresses on the standard port (80 or 443), and so must every redirect along the way. Files on an intranet, localhost or a private IP range cannot be fetched. The file also has to be downloadable without signing in, so a private repository’s archive link will not work. Publish the file somewhere public, or embed it as a data: URL or encoded_snip.

“A link in this import leads to a web page”. snip_uri points at the page that displays the file rather than at the file itself, so the import is refused. On GitHub, use the “Raw” link of a file, or the archive link of a repository, not the /blob/ address.

Nothing happens when the button is clicked. If your site sends a Content-Security-Policy header with a form-action directive, add https://app.inscrive.io to it, or the browser blocks the post. When you submit a form from JavaScript, call submit() directly in the click handler, not after an await, so the new tab is not treated as a pop-up.

413 Payload Too Large. The POST body is over 512 KB. Host the template as a zip and link it with snip_uri. For medium-sized sources, sending raw snip instead of encoded_snip cuts the overhead, because the source is encoded once instead of twice, and raw snip in a form with enctype="multipart/form-data" is sent with no encoding at all. Every field must still be text: a file upload field is rejected.

415 Unsupported Media Type. The endpoint reads only the two usual form encodings. Post a regular form (application/x-www-form-urlencoded or multipart/form-data), not JSON and not enctype="text/plain".

403 “Open this link or submit this form in the browser”. The request was sent by a script in the background (fetch(), navigator.sendBeacon, an <img>). Use a real link or a form submission instead.

429 Too Many Requests. A rate limit was hit. Most of the limits above are counted per client address, so visitors sharing one network address count together. A whole class importing at once from one campus network in a workshop can reach them, so spread such imports out. Otherwise, try again later.

503 Service Unavailable. Imports are briefly unavailable. Try again in a moment.

The snippet was wrapped in an article document, or was not. Decoration applies only to a single, non-zip .tex source without \documentclass. Include your own \documentclass if you want full control over the preamble.

The wrong engine compiles. Pass engine explicitly. Anything unrecognised falls back to detection from the preamble.

Differences from Overleaf

The endpoint is built to be a drop-in replacement, but it is not identical, and you should know where it differs:

  • visual_editor and rich_text have no effect. inscrive has one editor.
  • comment has no effect. inscrive never prepends a welcome comment to the imported source.
  • Unknown engine values fall back to automatic detection rather than to Overleaf’s default engine.
  • URL sources must resolve to public addresses on the standard port. Intranet URLs and addresses such as https://example.org:8443/… are refused.
  • A POST body is limited to 512 KB. Larger templates need to be hosted as a zip and linked.

For reference, Overleaf documents its side on its API page.

Badge

If you would rather show a button than a text link, use the hosted badge. It comes in a light and a dark version, both 160 × 36 pixels:

Open in Inscrive Open in Inscrive, dark version

As a link:

<a
	href="https://app.inscrive.io/docs?snip_uri=https%3A%2F%2Ftemplates.example.org%2Fthesis.zip"
	target="_blank"
	rel="noopener"
>
	<img src="https://inscrive.io/badges/open-in-inscrive.svg" alt="Open in Inscrive" height="36" />
</a>

As a form button, for any of the POST recipes above:

<form action="https://app.inscrive.io/docs" method="post" target="_blank">
	<input type="hidden" name="snip_uri" value="https://templates.example.org/thesis.zip" />
	<button type="submit" style="padding: 0; border: 0; background: none; cursor: pointer;">
		<img src="https://inscrive.io/badges/open-in-inscrive.svg" alt="Open in Inscrive" height="36" />
	</button>
</form>

For dark backgrounds, use https://inscrive.io/badges/open-in-inscrive-dark.svg. You are welcome to use either badge, unmodified, to link to inscrive, whether you load it from inscrive.io or keep a copy on your own server.

Running a template gallery or a LaTeX course and want to check your integration end to end? Create a free inscrive account, click your own button, and import one of your templates.

Further reading

Sign up for our newsletter

Roadmap progress, announcements and exclusive discounts — straight to your inbox.

We care about the protection of your data. Read our privacy policy.