An at.new link opens the AT Protocol composer with a record prefilled from the URL. The URL is the entire interface, so it can be built directly in any language. Opening a link never creates anything: the person who opens it reviews the record and presses Create.
| /<nsid> | Path form: open a template for that record type |
| collection | Same as the path form |
| rkey | Record key. Omit it to mint a fresh TID on create |
| record | The full record, percent-encoded JSON (small records) |
| #record=<payload> | The full record in the URL fragment (large records) |
| text | Becomes the record's text field |
| title | Added before the text |
| url | Added after the text, on its own line |
Examples:
https://at.new/xyz.statusphere.status https://at.new/?collection=app.bsky.feed.post&text=Hello%20world https://at.new/?record=%7B%22%24type%22%3A%22xyz.statusphere.status%22%2C%22status%22%3A%22%F0%9F%8E%89%22%7D
The record JSON includes $type — that selects the collection. If the record lacks a valid $type, a collection parameter fills it in. Records up to a few hundred bytes fit in ?record= as percent-encoded JSON. Anything larger goes in the fragment, #record=, which is processed in the opener's browser and never sent to a server. The limits are 100 KB encoded and 1 MB decoded.
The fragment accepts three encodings: percent-encoded JSON, b64: followed by base64url of the UTF-8 JSON, or dfl: followed by base64url of raw-deflate-compressed JSON. Base64url is base64 with + as -, / as _, and padding removed. Use b64: unless your records are large.
function atNewLink(record) {
const bytes = new TextEncoder().encode(JSON.stringify(record));
let binary = "";
for (let i = 0; i < bytes.length; i += 0x8000) {
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
}
const b64 = btoa(binary)
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return `https://at.new/#record=b64:${b64}`;
}For the compressed form: raw deflate has no zlib header. Node: zlib.deflateRawSync(buf).toString("base64url"). Python: zlib.compressobj(wbits=-15).
String values in the record can contain placeholders, which makes a link a reusable template. Built-ins are filled in when the opener presses Create:
| {{$now}} | the current time, as an ISO 8601 UTC instant |
| {{$did}} | the opener's signed-in DID |
| {{$handle}} | the opener's signed-in handle |
| {{$lang}} | the opener's primary language subtag, like "en" |
Any placeholder without a $, like {{message}}, becomes a text input the opener must fill before creating. Placeholders work inside longer strings and inside arrays.
atNewLink({
$type: "com.example.guestbook.entry",
message: "{{message}}",
author: "{{$did}}",
createdAt: "{{$now}}",
});Everyone who opens this link fills in their own message and creates the entry under their own DID, with a fresh timestamp.
More template variables coming soon.
Omit TID-style record keys so each opener gets a fresh key and repeat clicks never overwrite. Meaningful keys like self are kept.
A machine-readable version of these docs lives at /llms.txt. Give that URL to an AI agent and it can build these links.