blosstr docs
blosstr is a Blossom media server. Nostr clients upload to it with a signed event; apps can also use an API key from the dashboard. Every file is stored under its SHA-256 and served at https://blosstr.com/<sha256>.<ext>.
Quick start
- Get a plan for your pubkey on the pricing page.
- In your nostr client's media server settings, add
https://blosstr.com. - Attach a picture to a note. That's it.
From a script with an API key:
curl -X PUT https://blosstr.com/upload \ -H "Authorization: Bearer blosstr_…" \ -H "Content-Type: image/jpeg" \ --data-binary @photo.jpg
The response is a Blossom blob descriptor:
{
"url": "https://blosstr.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.jpg",
"sha256": "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553",
"size": 184292,
"type": "image/jpeg",
"uploaded": 1793000000,
"nip94": [["url", "…"], ["m", "image/jpeg"], ["x", "…"], ["size", "184292"]]
}Blossom endpoints
| Request | What it does |
|---|---|
GET /<sha256>[.ext] | The file. Supports Range requests. No auth. (BUD-01) |
HEAD /<sha256> | Whether the file exists, with its size and type. |
PUT /upload | Store the raw request body. Send X-SHA-256 to have the hash checked. Images are stripped of EXIF/GPS. (BUD-02) |
HEAD /upload | Preflight: send X-Content-Length, X-Content-Type and X-SHA-256 to find out whether an upload would be accepted before sending it. (BUD-06) |
PUT /media | Same as upload; images are always processed. (BUD-05) |
PUT /mirror | {"url": "https://…"}: blosstr fetches the file and stores it for you, up to 100 MB. (BUD-04) |
GET /list/<pubkey> | The pubkey's blobs, newest first. ?cursor=<sha256>&limit=, ?since/?until. Needs a signature from that pubkey. (BUD-12) |
DELETE /<sha256> | Delete a file you uploaded. |
Authentication
Every write, list and delete needs an Authorization header. Reads never do.
Signed events (nostr clients)
Authorization: Nostr <base64 event>, where the event is kind 24242 with a t tag for the verb (upload, list, delete, media), an expiration tag, and for uploads and deletes an x tag with the blob's hash (BUD-11). Any Blossom client library does this for you.
API keys (your backend)
Authorization: Bearer blosstr_…. A key acts as the pubkey that made it, with that pubkey's plan and storage. No signing needed.
API keys
Log in and open Dashboard → API keys to make one. A key is shown once; we only store its hash. Keys work only while the plan is active, and you can revoke one at any time.
Keys belong on a server. Don't put one in a web page or a mobile app: anyone who can read it can upload and delete as you.
The /v1 API
JSON endpoints for apps, all authenticated with an API key (a signed kind 24242 event works too).
| Request | Returns |
|---|---|
GET /v1/account | Plan, expiry, storage quota, largest file allowed, files and bytes used. |
GET /v1/files | {files, next_cursor}, newest first. ?limit=1..500, ?cursor, ?type=image|video|audio|other. |
GET /v1/files/<sha256> | One file, with download count and where it was mirrored from. |
DELETE /v1/files/<sha256> | Deletes it. |
GET /v1/usage?days=30 | Uploads and bytes per UTC day. |
const res = await fetch("https://blosstr.com/v1/files?type=image&limit=50", {
headers: { Authorization: `Bearer ${process.env.BLOSSTR_KEY}` },
});
const { files, next_cursor } = await res.json();Large files
A single request can carry up to about 100 MB. For bigger files (up to 1 GB), upload in chunks. The finished file is checked against the hash you give before it's kept.
POST /multipart/initiate {"filename", "contentType", "fileSize"}
→ {"sessionId", "chunkSize", "totalChunks"}
POST /multipart/<id>/chunk?partNumber=0 (raw bytes, exactly chunkSize except the last)
…
POST /multipart/<id>/complete {"finalHash": "<sha256 of the whole file>"}
→ blob descriptor
GET /multipart/<id> progress
DELETE /multipart/<id> abortEvery multipart request needs the same authorization as an upload, from the pubkey that started the session.
Errors
Failures use normal HTTP status codes. The reason is in the X-Reason header (as Blossom requires) and in the body as {"error": "…"}.
401: missing, expired or invalid authorization.402/403: no active plan for this pubkey, or not your file.413: the file is bigger than your plan allows, or would go over your storage.416: aRangeoutside the file.
Plans & payments
| Plan | Storage | Per file | Price |
|---|---|---|---|
basic | 20 GB | 1 GB | $240 / year |
creator | 100 GB | 1 GB | $360 / year |
advanced | 500 GB | 1 GB | $600 / year |
Plans are paid in sats over lightning at the current BTC/USD rate. Paying again adds a year to the time left and switches to the plan just paid for. Your app can sell plans for its users with these public endpoints:
GET https://blosstr.com/api/plans
→ {"plans": [{"id", "name", "yearly_usd", "yearly_sats", "storage_bytes", "max_file_size"}]}
POST https://blosstr.com/api/invoice {"pubkey": "npub… or hex", "plan": "creator"}
→ {"paymentHash", "bolt11", "amountSats", "amountUsd", "expiresAt", "paid": false}
GET https://blosstr.com/api/invoice/<paymentHash>
→ same shape; "paid": true once settled (checked with our wallet)
GET https://blosstr.com/api/subscription/<npub or hex>
→ {"active", "plan", "expiresAt", "subscribed"}Show the bolt11 to the user (QR code or WebLN), then poll the invoice every few seconds. Payments are credited even if the user closes the page.
Limits and rules
- Files up to 1 GB; mirrors up to 100 MB.
- Storage is counted per pubkey. Deleting a file frees its space.
- When a plan expires, files stay up but new uploads are refused until it's renewed.
- No illegal content. See the terms.