12<html lang="en">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width, initial-scale=1">6<title>NIP-96 - HTTP File Storage Integration — Nostr Instruction Manual</title>7<meta name="description" content="NIP-96: a deprecated REST API for HTTP file storage servers. Discovery document, upload, download, deletion, listing, and media transformations. Replaced by NIP-B7.">8<link rel="preload" href="../assets/fonts/michroma-400-latin.woff2" as="font" type="font/woff2" crossorigin>9<link rel="preload" href="../assets/fonts/exo2-var-latin.woff2" as="font" type="font/woff2" crossorigin>10<link rel="preload" href="../assets/fonts/sairacondensed-600-latin.woff2" as="font" type="font/woff2" crossorigin>11<link rel="preload" href="../assets/fonts/sairacondensed-700-latin.woff2" as="font" type="font/woff2" crossorigin>12<link rel="preload" href="../assets/fonts/sairacondensed-500-latin.woff2" as="font" type="font/woff2" crossorigin>13<link rel="preload" href="../assets/fonts/sharetechmono-400-latin.woff2" as="font" type="font/woff2" crossorigin>14<link rel="preload" href="../assets/fonts/exo2-400i-latin.woff2" as="font" type="font/woff2" crossorigin>15<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">16<link rel="stylesheet" href="../assets/manual.css">17</head>18<body>1920<svg width="0" height="0" style="position:absolute">21 <defs>22 <marker id="arr" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">23 <path d="M1,1 L8,4.5 L1,8 Z" fill="#f26a00"/>24 </marker>25 <marker id="arrb" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">26 <path d="M1,1 L8,4.5 L1,8 Z" fill="#2e5f9e"/>27 </marker>28 </defs>29</svg>3031<div class="page">32 <span class="staple s1"></span><span class="staple s2"></span>3334 <header class="band">35 <a class="logo" href="../index.html">NOSTR<b>▸</b>PROTOCOL MANUAL</a>36 <span class="booklet-note">Chapter 96 - Media</span>37 </header>38 <nav class="tabs">39 <a href="../index.html">Contents</a>40 <a href="#overview" class="on">Overview</a>41 <a href="#spec">Spec</a>42 <a href="#examples">Examples</a>43 <a href="#history">History</a>44 </nav>4546 <div class="chapter">47 <div class="nipno" data-n="96">96</div>48 <div class="titles">49 <div class="kicker">Nostr Implementation Possibility - Hex № 96</div>50 <h1>HTTP File Storage Integration</h1>51 <p class="sub">A REST API for file servers: discovery document, authenticated upload and delete, hash-addressed download. Deprecated in favor of Blossom.</p>52 </div>53 <div class="stamp">Deprecated</div>54 </div>5556 <main class="spread">57 <div class="main">5859 <h2 class="bar" id="overview"><span class="num">1</span> Overview</h2>6061 <div class="box danger">62 <span class="tag">Deprecated</span>63 <p>The source spec carries an <span class="chip unrecommended">unrecommended</span> warning. NIP-96 is deprecated in favor of <a href="b7.html">NIP-B7</a> (Blossom media). Do not build new file servers or clients on this API. This chapter documents the full spec for existing implementations.</p>64 </div>6566 <p>Nostr events are small. Pictures, video, and audio are not. This NIP defines a REST API for HTTP file storage servers that work with the Nostr network. A user uploads a file to a server. The server returns a URL. The user then references that URL in Nostr notes.</p>67 <p>The API does not use Nostr events over WebSockets to store, request, or retrieve data. This keeps the design simple: the file server never has to learn anything about relays. Identity still comes from Nostr. Uploads and deletes carry a <a href="98.html">NIP-98</a> HTTP auth event, so the server knows which pubkey owns each file.</p>68 <p>Every file is identified by the SHA-256 hash of its original content. The hash appears in the download URL and in the delete URL. A client that knows only the hash can try to fetch the file from any NIP-96 server.</p>6970 <figure class="diagram">71 <div class="frame">72 <svg viewBox="0 0 560 210" role="img" aria-label="Client discovers the server document, uploads a file over HTTP with NIP-98 auth, then publishes the returned URL to a relay">73 <rect class="d-actor" x="10" y="80" width="110" height="50" rx="8"/>74 <text class="d-actor-label" x="65" y="109" text-anchor="middle">Client</text>75 <rect class="d-actor" x="225" y="20" width="130" height="50" rx="8"/>76 <text class="d-actor-label" x="290" y="42" text-anchor="middle">File Server</text>77 <text class="d-note" x="290" y="60" text-anchor="middle">nip96.json + api_url</text>78 <rect class="d-actor" x="225" y="140" width="130" height="50" rx="8"/>79 <text class="d-actor-label" x="290" y="169" text-anchor="middle">Relay</text>80 <path class="d-msg d-flow" d="M122,92 C165,80 175,60 223,50"/>81 <path class="d-msg blue d-flow" d="M223,62 C180,74 170,88 122,100"/>82 <path class="d-msg d-flow" d="M122,118 C165,130 175,150 223,160"/>83 <text class="d-label" x="150" y="46" text-anchor="middle">POST file (NIP-98)</text>84 <text class="d-label" x="205" y="96" text-anchor="middle">url + ox hash</text>85 <text class="d-label" x="150" y="176" text-anchor="middle">note with url</text>86 <circle r="4" fill="#f26a00">87 <animateMotion dur="2.6s" repeatCount="indefinite" path="M122,92 C165,80 175,60 223,50"/>88 </circle>89 <circle r="4" fill="#2e5f9e">90 <animateMotion dur="2.6s" begin="1.3s" repeatCount="indefinite" path="M223,62 C180,74 170,88 122,100"/>91 </circle>92 </svg>93 </div>94 <figcaption>Fig. 96-1 — <b>Division of labor.</b> Files travel over HTTP. Only the resulting URL reaches the relays.</figcaption>95 </figure>9697 <h2 class="bar" id="spec"><span class="num">2</span> Server Adaptation: the Discovery Document</h2>98 <p>A file server opts in by serving an HTTPS route at <code>/.well-known/nostr/nip96.json</code>. This discovery document tells clients where the API lives and what the server offers. Only <code>api_url</code> is required.</p>99100 <div class="crt">101 <div class="titlebar">Discovery document - /.well-known/nostr/nip96.json</div>102<pre>{103 <span class="k">"api_url"</span>: <span class="s">"https://your-file-server.example/custom-api-path"</span>,104 <span class="k">"download_url"</span>: <span class="s">"https://a-cdn.example/a-path"</span>,105 <span class="k">"delegated_to_url"</span>: <span class="s">"https://your-file-server.example"</span>,106 <span class="k">"supported_nips"</span>: [<span class="c">60</span>],107 <span class="k">"tos_url"</span>: <span class="s">"https://your-file-server.example/terms-of-service"</span>,108 <span class="k">"content_types"</span>: [<span class="s">"image/jpeg"</span>, <span class="s">"video/webm"</span>, <span class="s">"audio/*"</span>],109 <span class="k">"plans"</span>: {110 <span class="k">"free"</span>: {111 <span class="k">"name"</span>: <span class="s">"Free Tier"</span>,112 <span class="k">"is_nip98_required"</span>: <span class="c">true</span>,113 <span class="k">"url"</span>: <span class="s">"https://..."</span>, <span class="c">// plan landing page, if any</span>114 <span class="k">"max_byte_size"</span>: <span class="c">10485760</span>,115 <span class="k">"file_expiration"</span>: [<span class="c">14, 90</span>], <span class="c">// range in days, 0 = no expiration</span>116 <span class="k">"media_transformations"</span>: { <span class="k">"image"</span>: [<span class="s">"resizing"</span>] }117 }118 }119}</pre>120 </div>121122 <div class="tscroll"><table class="ref">123 <caption>Discovery document fields</caption>124 <tr><th>Field</th><th>Required</th><th>Rule</th></tr>125 <tr><td class="btn">api_url</td><td>yes</td><td>Base URL for upload and deletion. Also serves downloads when <code>download_url</code> is absent or an empty string.</td></tr>126 <tr><td class="btn">download_url</td><td>no</td><td>Base URL for downloads. If absent, downloads use <code>api_url</code>.</td></tr>127 <tr><td class="btn">delegated_to_url</td><td>no</td><td>For relays only, never for HTTP servers. Points to a server that hosts its own <code>nip96.json</code>. See §3.</td></tr>128 <tr><td class="btn">supported_nips</td><td>no</td><td>List of NIP numbers the server supports. The source example lists <a href="60.html">NIP-60</a>.</td></tr>129 <tr><td class="btn">tos_url</td><td>no</td><td>Link to the terms of service.</td></tr>130 <tr><td class="btn">content_types</td><td>no</td><td>Accepted MIME types. Wildcards such as <code>audio/*</code> are allowed.</td></tr>131 <tr><td class="btn">plans</td><td>no</td><td>Map of plan objects, keyed by plan id. <code>free</code> is the only standardized key. Its presence tells clients the server offers free storage.</td></tr>132 </table></div>133134 <div class="tscroll"><table class="ref">135 <caption>Plan object fields</caption>136 <tr><th>Field</th><th>Rule</th></tr>137 <tr><td class="btn">name</td><td>Display name of the plan.</td></tr>138 <tr><td class="btn">is_nip98_required</td><td>Default <code>true</code>. Every plan MUST support <a href="98.html">NIP-98</a> uploads. Some plans may also allow uploads without it.</td></tr>139 <tr><td class="btn">url</td><td>The plan's landing page, if there is one.</td></tr>140 <tr><td class="btn">max_byte_size</td><td>Maximum file size in bytes.</td></tr>141 <tr><td class="btn">file_expiration</td><td>A two-element range in days. <code>[7, 0]</code> means 7 days to unlimited persistence. <code>[0, 0]</code> means no expiration. Early expiration may come from low traffic or any other factor.</td></tr>142 <tr><td class="btn">media_transformations</td><td>Map of media class to supported transformation names, such as <code>{"image": ["resizing"]}</code>.</td></tr>143 </table></div>144145 <div class="box tech">146 <span class="tag">Tech Note</span>147 <p>The source example sets <code>supported_nips</code> to <code>[60]</code>, but <a href="60.html">NIP-60</a> defines Cashu wallets and has no file-storage role. The field's semantics are otherwise undefined in the spec. Treat the example value as illustrative, not normative.</p>148 </div>149150 <h2 class="bar" id="relay-hints"><span class="num">3</span> Relay Hints</h2>151 <p>This section is for Nostr relays, not for HTTP servers. A relay MAY redirect to someone else's file storage server. To do this, the relay serves its own <code>/.well-known/nostr/nip96.json</code> with a <code>delegated_to_url</code> field. That field points to the URL where the real server hosts its own <code>nip96.json</code>. In this delegated document, <code>api_url</code> must be an empty string and all other fields must be absent.</p>152 <p>If the relay is itself an HTTP file storage server, it must use the <code>api_url</code> field instead.</p>153 <div class="box hint">154 <span class="tag">Hint!</span>155 <p>Looking for servers that speak this API? The source spec points to the community list at <code>github.com/aljazceru/awesome-nostr</code>, section "NIP-96 file storage servers". Kind 10096 (§9) records each user's own picks.</p>156 </div>157158 <h2 class="bar" id="auth"><span class="num">4</span> Auth</h2>159 <p>Where the spec says <strong>AUTH required</strong>, the client must add a <a href="98.html">NIP-98</a> <code>Authorization</code> header. The header carries a signed kind 27235 event. The client may optionally set the event's <code>payload</code> tag. When set, the value is the base64-encoded 256-bit SHA-256 hash of the file itself, not the hash of the whole request body.</p>160161 <h2 class="bar" id="upload"><span class="num">5</span> Upload</h2>162 <p><code>POST $api_url</code> as <code>multipart/form-data</code>. <strong>AUTH required.</strong></p>163 <div class="tscroll"><table class="ref">164 <caption>Upload form fields</caption>165 <tr><th>Field</th><th>Status</th><th>Rule</th></tr>166 <tr><td class="btn">file</td><td>REQUIRED</td><td>The file to upload.</td></tr>167 <tr><td class="btn">caption</td><td>RECOMMENDED</td><td>Loose description of the file.</td></tr>168 <tr><td class="btn">alt</td><td>RECOMMENDED</td><td>Strict description text for visibility-impaired users.</td></tr>169 <tr><td class="btn">expiration</td><td>optional</td><td>Unix timestamp in seconds. Empty string means store forever. The server is not required to honor it.</td></tr>170 <tr><td class="btn">size</td><td>optional</td><td>File byte size. Lets the server reject early when the file exceeds its limits.</td></tr>171 <tr><td class="btn">media_type</td><td>optional</td><td><code>"avatar"</code> or <code>"banner"</code>. Tells the server the file's role. If absent, the server treats it as a normal upload with no special treatment.</td></tr>172 <tr><td class="btn">content_type</td><td>optional</td><td>MIME type such as <code>"image/jpeg"</code>. Lets the server reject unsupported types early.</td></tr>173 <tr><td class="btn">no_transform</td><td>optional</td><td><code>"true"</code> asks the server to serve the file exactly as uploaded, with no transformation. The server may reject the request.</td></tr>174 </table></div>175 <p>Servers may accept other custom form fields. The server is not required to store any metadata the client sends. The server may ignore the embedded <code>filename</code> and store only the SHA-256 hash as the file name. The hash uniquely identifies the file, so the download and delete routes use it. The server MUST link the user's <code>pubkey</code> as the owner of the file, so the user can delete it later.</p>176 <p>Use <code>no_transform</code> to replicate one file to multiple servers for redundancy. Clients can read the server list event (§9) to find alternative servers that might hold the same file. When a client uploads with <code>no_transform</code>, it should check that the hash in the response matches. A mismatch means the server modified the file.</p>177178 <h3>Response codes</h3>179 <div class="tscroll"><table class="ref">180 <caption>Upload response codes</caption>181 <tr><th>Code</th><th>Meaning</th></tr>182 <tr><td class="btn">200 OK</td><td>The file already exists (existing hash), and the upload is successful.</td></tr>183 <tr><td class="btn">201 Created</td><td>Upload successful, new hash.</td></tr>184 <tr><td class="btn">202 Accepted</td><td>Upload is awaiting processing. See §6, Delayed Processing.</td></tr>185 <tr><td class="btn">400 Bad Request</td><td>Form data is invalid or not supported.</td></tr>186 <tr><td class="btn">402 Payment Required</td><td>The server requires payment. This flow is undefined.</td></tr>187 <tr><td class="btn">403 Forbidden</td><td>The user may not upload, or the file hash did not match the <code>payload</code> tag in the <code>Authorization</code> header.</td></tr>188 <tr><td class="btn">413 Payload Too Large</td><td>The file size exceeds the server limit.</td></tr>189 </table></div>190191 <h3>Response body</h3>192 <p>The upload response is a JSON object. The <code>nip94_event</code> field uses the <a href="94.html">NIP-94</a> event format, but the server does not need to fill <code>id</code>, <code>pubkey</code>, <code>created_at</code>, or <code>sig</code>. The field is absent when the upload fails.</p>193 <div class="crt">194 <div class="titlebar">Upload response - JSON</div>195<pre>{196 <span class="k">"status"</span>: <span class="s">"success"</span>, <span class="c">// "success" or "error"</span>197 <span class="k">"message"</span>: <span class="s">"Upload successful."</span>,198 <span class="k">"processing_url"</span>: <span class="s">"..."</span>, <span class="c">// optional, see Delayed Processing</span>199 <span class="k">"nip94_event"</span>: {200 <span class="k">"tags"</span>: [201 [<span class="s">"url"</span>, <span class="s">"https://your-file-server.example/custom-api-path/719171db…04b7b.png"</span>],202 [<span class="s">"ox"</span>, <span class="s">"719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b"</span>],203 [<span class="s">"x"</span>, <span class="s">"543244319525d9d08dd69cb716a18158a249b7b3b3ec4bbde5435543acb34443"</span>],204 [<span class="s">"m"</span>, <span class="s">"image/png"</span>],205 [<span class="s">"dim"</span>, <span class="s">"800x600"</span>]206 <span class="c">// ... other optional NIP-94 tags</span>207 ],208 <span class="k">"content"</span>: <span class="s">""</span>209 }210 <span class="c">// ... other custom fields</span>211}</pre>212 </div>213 <div class="tscroll"><table class="ref">214 <caption>nip94_event tags in the response</caption>215 <tr><th>Tag</th><th>Required</th><th>Rule</th></tr>216 <tr><td class="btn">url</td><td>yes</td><td>The download URL. Usually <code>download_url</code> (or <code>api_url</code> when absent) plus the original file hash. Appending the file extension is optional but strongly recommended: it helps clients detect the file type with a regular expression. The URL may also be any other address, for example for load balancing.</td></tr>217 <tr><td class="btn">ox</td><td>yes</td><td>SHA-256 hash of the ORIGINAL file, before any transformation. The server MUST store it: users download and delete the transformed file with this value.</td></tr>218 <tr><td class="btn">x</td><td>no</td><td>SHA-256 hash of the saved file after server transformations. The server can, but does not need to, store this value.</td></tr>219 <tr><td class="btn">m</td><td>no</td><td>MIME type. Recommended, so clients know the file type before download.</td></tr>220 <tr><td class="btn">dim</td><td>no</td><td>Pixel dimensions, such as <code>800x600</code>. Recommended, so clients can reserve UI space before download.</td></tr>221 </table></div>222 <p>If the server applied no transformation, <code>ox</code> and <code>x</code> hold the same value. The server MUST link the saved file to the hash of the <strong>original</strong> file, the <code>ox</code> value. That hash identifies the file for download and deletion.</p>223 <p>A client may upload the same file to one or many servers. After a successful upload, the client may fill in the missing fields and publish a real <a href="94.html">NIP-94</a> event to any set of relays. Alternatively, the client can skip NIP-94 and embed just the URL in a note. The client may also build an <code>imeta</code> tag (<a href="92.html">NIP-92</a>) from the <code>nip94_event</code> tags.</p>224225 <h2 class="bar" id="processing"><span class="num">6</span> Delayed Processing</h2>226 <p>A server may place an uploaded file in a queue for deferred processing. While processing runs, the server MUST serve the original file. When processing ends, the server swaps the original file for the processed one.</p>227 <p>The upload response has the usual shape, but some optional metadata, such as the <code>x</code> and <code>size</code> tags, is not yet available. Return every expected value that is known in advance. Example: if processing converts a JPEG to WebP, use a <code>.webp</code> extension in the <code>url</code> tag and set the <code>m</code> tag to <code>image/webp</code>. Omit metadata that is unknown before processing ends.</p>228 <p>The response MAY include a <code>processing_url</code>: a temporary URL the client polls to check progress. While processing runs, the server answers there with <strong>200 OK</strong> and this JSON:</p>229 <div class="crt">230 <div class="titlebar">Processing status - GET processing_url</div>231<pre>{232 <span class="k">"status"</span>: <span class="s">"processing"</span>, <span class="c">// "error" means the processing failed</span>233 <span class="k">"message"</span>: <span class="s">"Processing. Please check again later for updated status."</span>,234 <span class="k">"percentage"</span>: <span class="c">15</span> <span class="c">// integer, 0 to 100</span>235}</pre>236 </div>237 <p>When processing is over, the server answers at the same URL with <strong>201 Created</strong> and the regular success response from §5, now without a <code>processing_url</code> field. The response may include the metadata tags that were missing before.</p>238239 <figure class="diagram">240 <div class="frame">241 <svg viewBox="0 0 560 260" role="img" aria-label="Sequence diagram: client uploads, server replies 202 with processing_url, client polls until 201 Created">242 <line class="d-wire" x1="110" y1="40" x2="110" y2="245" stroke-dasharray="3 4"/>243 <line class="d-wire" x1="450" y1="40" x2="450" y2="245" stroke-dasharray="3 4"/>244 <rect class="d-actor" x="55" y="8" width="110" height="30" rx="6"/>245 <text class="d-actor-label" x="110" y="28" text-anchor="middle">Client</text>246 <rect class="d-actor" x="395" y="8" width="110" height="30" rx="6"/>247 <text class="d-actor-label" x="450" y="28" text-anchor="middle">Server</text>248 <path class="d-msg" d="M110,62 L445,62"/>249 <text class="d-label" x="280" y="55" text-anchor="middle">POST $api_url (file, NIP-98 auth)</text>250 <path class="d-msg blue" d="M450,95 L115,95"/>251 <text class="d-label" x="280" y="88" text-anchor="middle">202 Accepted + processing_url</text>252 <path class="d-msg" d="M110,128 L445,128"/>253 <text class="d-label" x="280" y="121" text-anchor="middle">GET processing_url</text>254 <path class="d-msg blue" d="M450,161 L115,161"/>255 <text class="d-label" x="280" y="154" text-anchor="middle">200 OK {"status":"processing","percentage":15}</text>256 <path class="d-msg" d="M110,194 L445,194"/>257 <text class="d-label" x="280" y="187" text-anchor="middle">GET processing_url (later)</text>258 <path class="d-msg blue d-flow" d="M450,227 L115,227"/>259 <text class="d-label" x="280" y="220" text-anchor="middle">201 Created + full nip94_event</text>260 </svg>261 </div>262 <figcaption>Fig. 96-2 — <b>Delayed processing.</b> The client polls <code>processing_url</code> until the server returns 201 Created.</figcaption>263 </figure>264265 <h3>File compression</h3>266 <p>The server may compress the file, strip metadata, or apply other transformations. For every file action, download and deletion alike, the <strong>original</strong> file's SHA-256 hash identifies the file in the URL string.</p>267268 <h2 class="bar" id="download"><span class="num">7</span> Download</h2>269 <p><code>GET $api_url/<sha256-hash>(.ext)</code></p>270 <p>The primary download URL from the upload response's <code>url</code> tag can be this standard URL or any non-standard URL the server wants. Either way, the server still MUST answer downloads at the standard URL. This lets a client try any NIP-96 server while knowing only the SHA-256 hash of the file.</p>271 <p>The <code><sha256-hash></code> part is the hash of the <strong>original</strong> file, not the transformed file. Servers must support the <code>.ext</code> file extension suffix. Clients may append it; this is optional but recommended. When present, the server may use the extension to pick the <code>Content-Type</code> header, for example <code>image/png</code> for <code>.png</code>. The extension may be absent: the hash alone identifies the file.</p>272 <div class="crt">273 <div class="titlebar">Download URL example</div>274<pre><span class="c">$api_url/</span>719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b<span class="k">.png</span></pre>275 </div>276277 <h3>Media transformations</h3>278 <p>Servers may respond to media transformation query parameters. A server ignores the parameters it does not support and serves the original file without transformation.</p>279 <p><strong>Image resizing</strong> is the one defined transformation. Upon upload, the server may create resized image variants, such as thumbnails, that respect the original aspect ratio. A client requests a width with the <code>w</code> query parameter, in pixels. The server serves the variant with the closest width, or generates a variant on the fly.</p>280 <div class="crt">281 <div class="titlebar">Resizing example</div>282<pre><span class="c">$api_url/<sha256-hash>.png</span><span class="k">?w=32</span></pre>283 </div>284285 <h2 class="bar" id="deletion"><span class="num">8</span> Deletion and Listing</h2>286 <p><code>DELETE $api_url/<sha256-hash>(.ext)</code>. <strong>AUTH required.</strong></p>287 <p>The hash is again the <strong>original</strong> file's hash. The extension is optional; the hash alone identifies the file. The server should reject deletes from users other than the original uploader with <code>403 Forbidden</code>.</p>288 <p>More than one user may have uploaded the same file, which yields the same hash. The server keeps one copy. A delete then must not remove the file. It only removes the requesting user's <code>pubkey</code> from the file's owner list.</p>289 <div class="crt">290 <div class="titlebar">Deletion response - 200 OK</div>291<pre>{292 <span class="k">"status"</span>: <span class="s">"success"</span>,293 <span class="k">"message"</span>: <span class="s">"File deleted."</span>294}</pre>295 </div>296297 <h3>Listing files</h3>298 <p><code>GET $api_url?page=x&count=y</code>. <strong>AUTH required.</strong> Returns the files linked to the authenticated user's pubkey. The <code>files</code> array contains <a href="94.html">NIP-94</a> events.</p>299 <div class="tscroll"><table class="ref">300 <caption>Listing query args</caption>301 <tr><th>Arg</th><th>Rule</th></tr>302 <tr><td class="btn">page</td><td>Page number. The offset is <code>page * count</code>.</td></tr>303 <tr><td class="btn">count</td><td>Number of items per page.</td></tr>304 </table></div>305 <div class="crt">306 <div class="titlebar">Listing response - JSON</div>307<pre>{308 <span class="k">"count"</span>: <span class="c">1</span>, <span class="c">// server page size: max(1, min(server_max_page_size, arg_count))</span>309 <span class="k">"total"</span>: <span class="c">1</span>, <span class="c">// total number of files</span>310 <span class="k">"page"</span>: <span class="c">0</span>, <span class="c">// current page number</span>311 <span class="k">"files"</span>: [312 {313 <span class="k">"tags"</span>: [314 [<span class="s">"ox"</span>, <span class="s">"719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b"</span>],315 [<span class="s">"x"</span>, <span class="s">"5d2899290e0e69bcd809949ee516a4a1597205390878f780c098707a7f18e3df"</span>],316 [<span class="s">"size"</span>, <span class="s">"123456"</span>],317 [<span class="s">"alt"</span>, <span class="s">"a meme that makes you laugh"</span>],318 [<span class="s">"expiration"</span>, <span class="s">"1715691139"</span>]319 <span class="c">// ...other metadata</span>320 ],321 <span class="k">"content"</span>: <span class="s">"haha funny meme"</span>, <span class="c">// caption</span>322 <span class="k">"created_at"</span>: <span class="c">1715691130</span> <span class="c">// upload timestamp</span>323 }324 ]325}</pre>326 </div>327328 <h2 class="bar" id="server-list"><span class="num">9</span> Selecting a Server</h2>329 <p>This section is for client developers. Server developers may skip it. A File Server Preference event is a kind <strong>10096</strong> replaceable event. It selects one or more servers the user wants to upload files to. Each server appears as a <code>server</code> tag:</p>330 <div class="crt">331 <div class="titlebar">Kind 10096 - File Server Preference</div>332<pre>{333 <span class="k">"kind"</span>: <span class="c">10096</span>,334 <span class="k">"content"</span>: <span class="s">""</span>,335 <span class="k">"tags"</span>: [336 [<span class="s">"server"</span>, <span class="s">"https://file.server.one"</span>],337 [<span class="s">"server"</span>, <span class="s">"https://file.server.two"</span>]338 ]339 <span class="c">// other fields...</span>340}</pre>341 </div>342 <p>Kind 10096 sits in the replaceable range of <a href="01.html">NIP-01</a>. A relay keeps only the latest event per pubkey.</p>343344 <h2 class="bar" id="examples"><span class="num">10</span> Worked Example</h2>345 <p>The transcript below shows one full cycle: discovery, upload, download, delete. Orange lines are client requests. Blue lines are server responses.</p>346 <div class="crt">347 <div class="titlebar">HTTP session - files.example.com</div>348<pre><span class="c">-- 1. discover the API --</span>349<span class="send">→ GET /.well-known/nostr/nip96.json</span>350<span class="recv">← 200 {"api_url":"https://files.example.com/api",351 "plans":{"free":{"name":"Free Tier","is_nip98_required":true,352 "max_byte_size":10485760,"file_expiration":[0,0],353 "media_transformations":{"image":["resizing"]}}}}</span>354355<span class="c">-- 2. upload with NIP-98 auth --</span>356<span class="send">→ POST /api (multipart: file=cat.png, caption="my cat", alt="a gray cat")357 Authorization: Nostr eyJraW5kIjoyNzIzNSw…</span>358<span class="recv">← 201 {"status":"success","message":"Upload successful.",359 "nip94_event":{"tags":[360 ["url","https://files.example.com/api/719171db…04b7b.png"],361 ["ox","719171db…04b7b"],["x","719171db…04b7b"],362 ["m","image/png"],["dim","800x600"]],"content":""}}</span>363364<span class="c">-- 3. anyone downloads a 32px thumbnail --</span>365<span class="send">→ GET /api/719171db…04b7b.png?w=32</span>366<span class="recv">← 200 Content-Type: image/png (32px-wide variant)</span>367368<span class="c">-- 4. the owner deletes --</span>369<span class="send">→ DELETE /api/719171db…04b7b.png370 Authorization: Nostr eyJraW5kIjoyNzIzNSw…</span>371<span class="recv">← 200 {"status":"success","message":"File deleted."}</span></pre>372 </div>373 <p>The server applied no transformation to the stored file, so <code>ox</code> and <code>x</code> match. After step 2 the client can publish a <a href="94.html">NIP-94</a> event, or paste the URL into a note, or build a <a href="92.html">NIP-92</a> <code>imeta</code> tag.</p>374375 <h2 class="bar" id="history"><span class="num">11</span> Version History</h2>376 <p>NIP-96 lived about twenty months as the standard upload API before Blossom replaced it. Notable changes, newest first:</p>377 <ul class="patchnotes">378 <li><span class="ver">2025</span><span class="when">2025-09</span>Deprecated in favor of <a href="b7.html">NIP-B7</a> (Blossom media). The <code>unrecommended</code> warning landed in the body in 2026-05.</li>379 <li><span class="ver">2024</span><span class="when">2024-05</span>Added the file listing endpoint with <code>page</code>/<code>count</code> pagination, and the <code>no_transform</code> upload field for byte-exact replication.</li>380 <li><span class="ver">2024</span><span class="when">2024-03</span>Simplified the server list: the in-spec table gave way to a link to the community list.</li>381 <li class="minor"><span class="ver">2024</span><span class="when">2024</span>Removed a temporary NIP-54 mention, dropped the authors line, plus typo and formatting fixes.</li>382 <li><span class="ver">Origin</span><span class="when">2024-01-08</span>arthurfranca opened the spec as "NIP-96 - HTTP File Storage Integration" (#547).</li>383 </ul>384385 <div class="credits">386 <h3 class="cardhead">Original Proposal - Credits</h3>387 <table>388 <tr><td>Author</td><td>arthurfranca</td></tr>389 <tr><td>First commit</td><td>2024-01-08 — “NIP-96 - HTTP File Storage Integration (#547)” - <a href="https://github.com/nostr-protocol/nips/pull/547">PR #547</a></td></tr>390 <tr><td>Status</td><td>draft - unrecommended - optional — deprecated in favor of <a href="b7.html">NIP-B7</a></td></tr>391 <tr><td>Source</td><td><a href="https://github.com/nostr-protocol/nips/blob/master/96.md">96.md on GitHub</a> - <a href="https://github.com/nostr-protocol/nips/commits/master/96.md">full change history</a></td></tr>392 </table>393 </div>394395 <h2 class="bar"><span class="num">12</span> See Also</h2>396 <ul>397 <li><a href="b7.html">NIP-B7</a> — Blossom media, the replacement for this NIP.</li>398 <li><a href="98.html">NIP-98</a> — the HTTP auth events that protect upload, delete, and listing.</li>399 <li><a href="94.html">NIP-94</a> — file metadata events; the upload response reuses this format.</li>400 <li><a href="92.html">NIP-92</a> — <code>imeta</code> tags built from the upload response.</li>401 <li><a href="01.html">NIP-01</a> — base event rules; kind 10096 follows its replaceable-event storage class.</li>402 </ul>403 </div>404405 <aside>406 <div class="cartridge">407 <div class="inner">408 <h3 class="cardhead">Spec Card</h3>409 <div class="lights"><span class="led err"></span><span class="led err"></span><span class="led err"></span></div>410 <dl>411 <dt>Status</dt><dd><span class="chip draft">draft</span><span class="chip unrecommended">unrecommended</span></dd>412 <dt>Defines</dt><dd>nip96.json discovery doc - upload/download/delete/list API - ox/x tags - kind 10096</dd>413 <dt>Kind(s)</dt><dd>10096 (replaceable)</dd>414 <dt>Requires</dt><dd><a href="01.html">NIP-01</a> - <a href="98.html">NIP-98</a> - <a href="94.html">NIP-94</a> format</dd>415 <dt>Transport</dt><dd>HTTPS - multipart/form-data - JSON</dd>416 <dt>Crypto</dt><dd>SHA-256 file hashes</dd>417 <dt>Replaced by</dt><dd><a href="b7.html">NIP-B7</a> Blossom media</dd>418 <dt>First commit</dt><dd>2024-01-08</dd>419 <dt>Last updated</dt><dd>2026-06-13</dd>420 </dl>421 </div>422 </div>423 </aside>424 </main>425426 <nav class="navbtns">427 <a href="94.html">◀ NIP-94: File Metadata</a>428 <a href="98.html">NIP-98: HTTP Auth ▶</a>429 </nav>430431 <footer class="pagefoot">432 <a href="../index.html">Nostr Protocol Manual</a>433 <span class="pageno">NIP 96 / F4</span>434 </footer>435</div>436437<script src="../assets/webmcp.js" defer></script>438</body>439</html>