AnonDrop
Dashboard Docs Pricing Search AI Wallet Info
Developer hub

Build on AnonDrop

Plain HTTP, no SDK anywhere. Storage speaks S3 so your existing tools work unchanged, and the same buckets are reachable over a small JSON REST API. Accounts and billing are a handful of endpoints. Object events arrive as signed webhooks you can verify offline.

Step one

Getting started

Your user key

AnonDrop has no sign-up form. Every browser holds a user key, and that key is the account. It lives in localStorage under userkey, and a GET /register issues a fresh one. This is exactly what the upload page does:

if (!localStorage.getItem('userkey')) {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', '/register');
    xhr.onload = () => {
        const m = xhr.responseText.match(/localStorage\.setItem\('userkey', '([0-9]+)'\)/);
        if (m) localStorage.setItem('userkey', m[1]);
    };
    xhr.send();
}

Outside the browser, keep the key in an environment variable and pass it as ?key=. It is a bearer credential: anyone holding it holds the account, so treat it the way you would treat a password.

export ANONDROP_KEY=1234567890123456789

curl -s "https://anondrop.net/api/account?key=$ANONDROP_KEY"

Access keys for storage

The S3 API uses a different credential: an access key pair, minted from the user key and signed with AWS Signature Version 4. Key ids begin with AKAD, the secret is shown once, and each key can be scoped to read only or to a named list of buckets. Mint them on the account page or through POST /api/account/keys below. Storage is part of Pro and Business.

Conventions

  • Both anondrop.net and anondrop.io serve the same account, the same buckets and the same endpoints.
  • The same buckets are also reachable over a JSON REST API at /api/v1, using the same access keys with no signing at all: send them as Authorization: Bearer KEYID:SECRET, or as the header pair X-AnonDrop-Key and X-AnonDrop-Secret.
  • Request bodies are JSON with Content-Type: application/json. Responses are JSON.
  • Byte counts are bytes, timestamps are Unix seconds, and money is USD while the amount you send is SOL.
  • Errors come back as {"error": {"code": "...", "message": "..."}} with a matching HTTP status. 401 means the user key was missing or not recognised, 402 means the plan does not include that feature.

GET /api/v1/ping is the quickest way to confirm an access key works:

curl -s "https://anondrop.net/api/v1/ping" \
     -H "Authorization: Bearer AKADIVMECTKQJRCUCQ2DJ5KU4VCJIQXGK6DBNVYGYZI:wJalrXUtnFEMI-K7MDENG-bPxRfiCYEXAMPLEKEY"
{
  "ok": true,
  "plan": "pro",
  "plan_name": "Pro",
  "key": "AKADIVMECTKQJRCUCQ2DJ5KU4VCJIQXGK6DBNVYGYZI",
  "time": 1771286400
}
Reference

Account API

/api/plans and /api/rate are public. Everything else takes your user key as ?key=.

GET/api/plans

The full catalogue, the comparison table used on the pricing page, and the SOL rate the checkout will quote. No parameters.

Request

curl -s "https://anondrop.net/api/plans"

Response

{
  "plans": [
    {
      "id": "pro",
      "name": "Pro",
      "tagline": "S3-compatible storage for builders, at a fraction of the price.",
      "price": {"monthly": 4.99, "yearly": 49.0, "lifetime": 149.0},
      "storage": 2199023255552,
      "storage_label": "2 TB",
      "egress": 5497558138880,
      "egress_label": "5 TB",
      "max_object_label": "5 TB",
      "speed_label": "Uncapped",
      "api_keys": "10",
      "buckets": 25,
      "seats": 1,
      "rate_limit": 1200,
      "features": {
        "s3": true, "rest_api": true, "private_objects": true,
        "signed_urls": true, "passwords": true, "expiry": true,
        "download_limits": true, "webhooks": true, "analytics": true,
        "priority": true, "bulk": true, "support": "email"
      },
      "highlights": ["2 TB storage and 5 TB transfer every month"]
    }
  ],
  "comparison": [
    {"name": "AnonDrop Pro", "storage_gb": 0.0025, "egress_gb": 0.0,
     "note": "2 TB storage and 5 TB transfer for 4.99 per month", "ours": true}
  ],
  "cycles": [
    {"id": "monthly", "label": "Monthly", "days": 31},
    {"id": "yearly", "label": "Yearly", "days": 366},
    {"id": "lifetime", "label": "Lifetime", "days": 0}
  ],
  "rate": {"sol_usd": 168.42, "updated": 1770000000}
}

The sample shows one plan and one comparison row. The live response lists Free, Pro and Business in that order, with the full highlight arrays.

GET/api/rate

The SOL price in USD, and when it was last refreshed. This is the rate a checkout created right now would use.

Request

curl -s "https://anondrop.net/api/rate"

Response

{"sol_usd": 168.42, "updated": 1770000000}
GET/api/account?key=USERKEY

Everything about the account in one call: plan, allowances, live usage for the current month, access keys, buckets, webhooks, payment history and your SOL balance.

Parameters

  • key — your user key. Required.

Request

curl -s "https://anondrop.net/api/account?key=$ANONDROP_KEY"

Response

{
  "plan": "pro",
  "plan_name": "Pro",
  "expires": 1801526400,
  "cycle": "yearly",
  "auto_renew": false,
  "limits": {
    "storage": 2199023255552,
    "egress": 5497558138880,
    "max_object": 5497558138880,
    "api_keys": 10,
    "buckets": 25,
    "seats": 1,
    "rate_limit": 1200
  },
  "features": {
    "s3": true, "rest_api": true, "private_objects": true,
    "signed_urls": true, "passwords": true, "expiry": true,
    "download_limits": true, "webhooks": true, "analytics": true,
    "priority": true, "bulk": true, "support": "email"
  },
  "usage": {
    "stored": 41234567890,
    "objects": 1842,
    "egress": 9123456789,
    "ingress": 41234567890,
    "requests": 20431,
    "month": "2026-08"
  },
  "keys": [
    {
      "id": "AKADIVMECTKQJRCUCQ2DJ5KU4VCJIQXGK6DBNVYGYZI",
      "name": "ci-deploy",
      "perms": ["read", "write"],
      "buckets": ["releases"],
      "created": 1769990000,
      "last_used": 1771286400
    }
  ],
  "buckets": [
    {"name": "releases", "created": 1769990000, "objects": 1842, "bytes": 41234567890}
  ],
  "webhooks": [
    {
      "id": "7c1f9a2b4e5d",
      "url": "https://example.com/hooks/anondrop",
      "events": ["object.created", "object.deleted"],
      "active": true
    }
  ],
  "payments": [
    {
      "plan": "pro", "cycle": "yearly", "usd": 49.0, "sol": 0.29094,
      "rate": 168.42, "at": 1769990000,
      "signature": "2Xy4TVgwhH8cKT9KZnGXqbdtPLuw79bsPrMtFdJaxpLJaMqzngfpgTX9d47ej6zxwZvCBFw6UxPbwcLzCaAjPsZ5"
    }
  ],
  "balance_sol": 0.412
}

Revoked keys are omitted, payments are newest first, and access key secrets are never returned here. last_used is rounded to the day on purpose: we count usage, we do not keep request logs.

POST/api/account/keys?key=USERKEY

Mints an S3 access key. The secret is returned once and never again, so store it as you receive it. Pro or Business only; on Free the call is refused with 402 upgrade_required.

Body

  • name — a label for your own use, trimmed to 64 characters.
  • perms — any of read and write. Defaults to both; anything else is dropped.
  • buckets — restrict the key to these bucket names, up to 50 of them. An empty list means every bucket on the account.

Request

curl -s -X POST "https://anondrop.net/api/account/keys?key=$ANONDROP_KEY" \
     -H "Content-Type: application/json" \
     -d '{"name": "ci-deploy", "perms": ["read", "write"], "buckets": ["releases"]}'

Response

{
  "id": "AKADIVMECTKQJRCUCQ2DJ5KU4VCJIQXGK6DBNVYGYZI",
  "secret": "wJalrXUtnFEMI-K7MDENG-bPxRfiCYEXAMPLEKEY",
  "name": "ci-deploy",
  "perms": ["read", "write"],
  "buckets": ["releases"],
  "created": 1771286400
}

Reaching the key limit for your plan answers 409 key_limit.

POST/api/account/keys/delete?key=USERKEY

Revokes an access key. It stops working immediately, everywhere, including presigned URLs it signed that have not expired yet.

Request

curl -s -X POST "https://anondrop.net/api/account/keys/delete?key=$ANONDROP_KEY" \
     -H "Content-Type: application/json" \
     -d '{"id": "AKADIVMECTKQJRCUCQ2DJ5KU4VCJIQXGK6DBNVYGYZI"}'

Response

{"ok": true}
POST/api/account/webhooks?key=USERKEY

Registers an endpoint for object events. Pro or Business only.

Body

  • url — the URL we POST to. It must be https, at most 400 characters, and must resolve to a public address.
  • events — any of object.created and object.deleted. Anything else is dropped, and an empty list is read as ["object.created"].

Request

curl -s -X POST "https://anondrop.net/api/account/webhooks?key=$ANONDROP_KEY" \
     -H "Content-Type: application/json" \
     -d '{"url": "https://example.com/hooks/anondrop",
          "events": ["object.created", "object.deleted"]}'

Response

{
  "id": "7c1f9a2b4e5d",
  "url": "https://example.com/hooks/anondrop",
  "events": ["object.created", "object.deleted"]
}

There is no per-endpoint secret to store: deliveries are signed with our published key, so verification needs nothing but the public key below. An account may hold 20 endpoints; past that the call answers 409 limit.

POST/api/account/webhooks/delete?key=USERKEY

Removes a webhook. Deliveries stop at once.

Request

curl -s -X POST "https://anondrop.net/api/account/webhooks/delete?key=$ANONDROP_KEY" \
     -H "Content-Type: application/json" \
     -d '{"id": "7c1f9a2b4e5d"}'

Response

{"ok": true}
POST/api/account/autorenew?key=USERKEY

Turns automatic renewal on or off for the current plan.

Request

curl -s -X POST "https://anondrop.net/api/account/autorenew?key=$ANONDROP_KEY" \
     -H "Content-Type: application/json" \
     -d '{"enabled": true}'

Response

{"ok": true, "auto_renew": true}
Reference

Billing API

Plans are priced in USD and paid in SOL. A checkout converts the price at the live rate and locks that SOL amount for the life of the intent — the rate moving afterwards changes nothing about what you owe. There are three ways to settle an intent: a browser wallet, a Solana Pay QR or address, or your AnonDrop balance.

POST/api/billing/checkout?key=USERKEY

Opens a payment intent. Everything needed for all three payment routes comes back in one response.

Body

  • planpro or business.
  • cyclemonthly, yearly, or lifetime for Pro.

Request

curl -s -X POST "https://anondrop.net/api/billing/checkout?key=$ANONDROP_KEY" \
     -H "Content-Type: application/json" \
     -d '{"plan": "pro", "cycle": "yearly"}'

Response

{
  "intent": "9f4c2a7e1b6d5c8a4b3e2f10",
  "plan": "pro",
  "plan_name": "Pro",
  "cycle": "yearly",
  "usd": 49.0,
  "sol": 0.29094,
  "rate": 168.42,
  "address": "F7xTEsBoZNddHQRiCkg6BYViMQySvzHm6wAY4EQYbKhE",
  "reference": "GBf9UZoP5Brk2jYWeHjwKwz49Ea6uAqG5HJJtJzajuzG",
  "pay_url": "solana:F7xTEsBoZNddHQRiCkg6BYViMQySvzHm6wAY4EQYbKhE?amount=0.29094&reference=GBf9UZoP5Brk2jYWeHjwKwz49Ea6uAqG5HJJtJzajuzG&label=AnonDrop&message=Pro%20Yearly%20plan",
  "qr_svg": "<svg xmlns=... ></svg>",
  "status": "pending",
  "expires": 1770001800,
  "balance_sol": 0.412
}

Fields

  • usd and sol — show both. rate is the SOL price the conversion used.
  • address and reference — where to send, and the marker that ties a transfer to this intent.
  • pay_url and qr_svg — a Solana Pay link and a ready-made SVG QR you can drop straight into the page.
  • expires — an intent, and the rate it locked, lives 30 minutes. Open a new one after that.
  • balance_sol — the AnonDrop balance available right now, so you know whether route (c) is possible.

An unavailable rate answers 503 rate_unavailable, and a plan and cycle that is not sold, such as a Business lifetime, answers 400 invalid_plan.

POST/api/billing/tx

Route (a), browser wallet. Builds an unsigned Solana transaction for the connected wallet to sign. No key parameter: the intent identifies the account.

Body

  • intent — the intent id from checkout.
  • payer — the base58 public key of the connected wallet.

Request

curl -s -X POST "https://anondrop.net/api/billing/tx" \
     -H "Content-Type: application/json" \
     -d '{"intent": "9f4c2a7e1b6d5c8a4b3e2f10",
          "payer": "Atgt3SLoEEXoVKk1fGLrXpzR1ybA4QcTtS991TuKAsiu"}'

Response

{
  "transaction": "AQABA0Zk...base64 unsigned transaction...AAAA",
  "sol": 0.29094,
  "address": "F7xTEsBoZNddHQRiCkg6BYViMQySvzHm6wAY4EQYbKhE"
}
POST/api/billing/confirm

Hands us the signature the wallet returned. We verify it on chain and activate the plan.

Request

curl -s -X POST "https://anondrop.net/api/billing/confirm" \
     -H "Content-Type: application/json" \
     -d '{"intent": "9f4c2a7e1b6d5c8a4b3e2f10",
          "signature": "2Xy4TVgwhH8cKT9KZnGXqbdtPLuw79bsPrMtFdJaxpLJaMqzngfpgTX9d47ej6zxwZvCBFw6UxPbwcLzCaAjPsZ5"}'

Response

{"ok": true, "plan": "pro", "expires": 1801526400}

A transaction that has not settled yet answers 202 not_confirmed; it applies on its own once it lands, so treat that as "keep waiting" rather than an error. A signature that already paid for something answers 409 already_used.

GET/api/billing/status/<intent>

Route (b), QR or address. Poll this while the customer scans the QR or copies the address and amount. status is pending, paid or expired, and plan and expires appear once it is paid. An intent we have never heard of answers 404 unknown_intent.

Request

curl -s "https://anondrop.net/api/billing/status/9f4c2a7e1b6d5c8a4b3e2f10"

Response

{"status": "paid", "plan": "pro", "expires": 1801526400}
POST/api/billing/balance?key=USERKEY

Route (c), pay from your AnonDrop wallet. Only offered when balance_sol from checkout covers the sol amount. Settles instantly, with no chain wait. Too small a balance answers 402 insufficient_balance.

Request

curl -s -X POST "https://anondrop.net/api/billing/balance?key=$ANONDROP_KEY" \
     -H "Content-Type: application/json" \
     -d '{"intent": "9f4c2a7e1b6d5c8a4b3e2f10"}'

Response

{"ok": true, "plan": "pro", "expires": 1801526400}

End to end in the browser

Open the intent, let the wallet sign, confirm. Phantom and Solflare expose the same two methods, so one path covers both:

import { Transaction } from '@solana/web3.js';

const key = localStorage.getItem('userkey');

const intent = await (await fetch('/api/billing/checkout?key=' + key, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ plan: 'pro', cycle: 'yearly' })
})).json();

const provider = window.solana || window.solflare;
const account = await provider.connect();

const built = await (await fetch('/api/billing/tx', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        intent: intent.intent,
        payer: account.publicKey.toString()
    })
})).json();

const raw = Uint8Array.from(atob(built.transaction), c => c.charCodeAt(0));
const signed = await provider.signAndSendTransaction(Transaction.from(raw));

const done = await (await fetch('/api/billing/confirm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ intent: intent.intent, signature: signed.signature })
})).json();

console.log(done.plan, done.expires);

If the customer would rather scan, render qr_svg, show the exact sol amount next to the usd price it was converted from, and poll until the intent reports paid:

const poll = setInterval(async () => {
    const s = await (await fetch('/api/billing/status/' + intent.intent)).json();
    if (s.status === 'paid') {
        clearInterval(poll);
        location.reload();
    } else if (s.status === 'expired') {
        clearInterval(poll);
    }
}, 4000);

Always display both numbers. The customer agreed to a USD price and sends a SOL amount, and showing the pair with the rate that produced it is what makes the payment feel exact rather than approximate.

Events

Webhooks

Register an endpoint and we POST an event to it whenever an object changes. Delivery is best effort and completely off the request path, so a slow or unreachable endpoint of yours never slows down or fails a storage request. Webhooks are part of Pro and Business.

Events

EventRaised by
object.createdPutObject and CompleteMultipartUpload on the S3 API, and an object PUT on the REST API.
object.deletedDeleteObject on the S3 API, and an object DELETE on the REST API.

Multi-key DeleteObjects and CopyObject do not raise events. A registration that names no events is read as object.created.

Delivery

We POST the event as JSON. The body is the canonical form of the payload — keys sorted, no whitespace between separators — and those exact bytes are what the signature covers, so you verify the raw body without re-serialising anything first.

POST /hooks/anondrop HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: AnonDrop-Webhooks/1
X-AnonDrop-Event: object.created
X-AnonDrop-Delivery: 6f1c2d84-9a3b-4e57-8c10-2b7fd9e4a615
X-AnonDrop-Timestamp: 1771286400
X-AnonDrop-Hook: 7c1f9a2b4e5d
X-AnonDrop-Algorithm: ed25519
X-AnonDrop-Key: 8DHfUSYeYWSHG1iHiq312SPgoV8vQh0nnDZCeDMSwAw=
X-AnonDrop-Signature: vs6Hs4KYpqe4P1tIYyvVqc0kAckcedAaxNOXHY+xyUad8iO/oeCkf7e/EZWGnndz7DRbk0/QcuJ07sYZb9W2Cg==

{"at":1771286400,"bucket":"releases","etag":"9f86d081884c7d659a2feaa0c55ad015","event":"object.created","key":"builds/release.tar.gz","size":184320,"type":"anondrop.webhook.v1"}

Payload

  • event — the event name.
  • bucket and key — where it happened.
  • size — the object size in bytes.
  • etag — the object ETag, without quotes.
  • at — when it happened, Unix seconds.
  • type — always anondrop.webhook.v1. It marks the document as a webhook event and nothing else, so a handler that also processes checkout receipts can tell the two apart before it looks at anything else.

Headers

  • X-AnonDrop-Signature — base64 Ed25519 signature over the raw request body.
  • X-AnonDrop-Key and X-AnonDrop-Algorithm — the public key that signed it, and ed25519. Verify against your own pinned copy of the key rather than the one in the header.
  • X-AnonDrop-Delivery — a unique id for this attempt's event, useful as an idempotency key.
  • X-AnonDrop-Event and X-AnonDrop-Timestamp — the event name and its timestamp, so you can route or age-check before parsing.
  • X-AnonDrop-Hook — which of your registered endpoints this delivery belongs to.

Signatures

Every delivery is signed with Ed25519, over the raw request body exactly as it arrives. Webhooks have their own signing key, separate from the checkout callback key documented on Info / API: the webhook key verifies webhook deliveries and nothing else, and the checkout key verifies receipts and nothing else, so a signed event can never be replayed as a signed payment. The webhook key is published at /webhookpubkey as JSON with public_key_base64, public_key_hex and algorithm:

8DHfUSYeYWSHG1iHiq312SPgoV8vQh0nnDZCeDMSwAw=

Verify the bytes you received, not a re-encoding of them. The delivery above is a real signature over its own body: paste it into either sample and it passes.

Verify in Python

import base64, json
from nacl.signing import VerifyKey

PUBLIC_KEY_B64 = "8DHfUSYeYWSHG1iHiq312SPgoV8vQh0nnDZCeDMSwAw="
VERIFIER = VerifyKey(base64.b64decode(PUBLIC_KEY_B64))

def handle(raw_body, headers):
    VERIFIER.verify(raw_body, base64.b64decode(headers["X-AnonDrop-Signature"]))
    event = json.loads(raw_body)
    print(event["event"], event["bucket"], event["key"], event["size"])
    return event

On Flask or Quart that is request.get_data(); on Django it is request.body. Anything that hands you a parsed dict instead of bytes will not verify, because the whitespace is gone.

Verify in Node.js

const express = require("express");
const nacl = require("tweetnacl");

const PUBLIC_KEY_B64 = "8DHfUSYeYWSHG1iHiq312SPgoV8vQh0nnDZCeDMSwAw=";
const PUBLIC_KEY = Buffer.from(PUBLIC_KEY_B64, "base64");

const app = express();

app.post("/hooks/anondrop", express.raw({ type: "application/json" }), (req, res) => {
    const ok = nacl.sign.detached.verify(
        req.body,
        Buffer.from(req.get("X-AnonDrop-Signature") || "", "base64"),
        PUBLIC_KEY
    );
    if (!ok) return res.status(400).send("bad signature");

    const event = JSON.parse(req.body.toString("utf8"));
    console.log(event.event, event.bucket, event.key, event.size);
    res.sendStatus(200);
});

app.listen(3000);

Verify before you act. The signature is what proves the event came from AnonDrop rather than from anyone who guessed your endpoint, and it is checked offline against a public key, so nothing has to be called back to confirm it.

Retries

  • Up to 3 attempts, each capped at 8 seconds, and never more than 20 seconds in total across all of them.
  • A pause of 1 second then 3 seconds between attempts.
  • Any 2xx is success. A 5xx, a timeout or a connection failure is retried; a 4xx stops delivery straight away, because your endpoint understood us and said no.
  • Redirects are not followed, and a redirect counts as a refusal.
  • Your endpoint can never slow one of our requests down: delivery runs off the request path entirely and a failure is never visible to the caller who triggered it.

Good practice

  • Answer quickly with any 2xx and do the real work after. A slow endpoint burns its budget and sees retries.
  • Treat deliveries as at-least-once and make your handler idempotent, keyed on X-AnonDrop-Delivery.
  • Reject anything whose at is far from now, so an old capture cannot be replayed at you.
  • Pin the public key in your code. Never trust the copy in X-AnonDrop-Key to verify the message it arrived with.
  • Registration requires https and a hostname that resolves to a public address, so a private or loopback destination is refused up front.
Fair use

Rate limits

Limits are per account, per minute, and reset at the top of the next minute. The storage API answers a request over the ceiling with 503 SlowDown, which every S3 client already retries with backoff.

PlanRequests per minuteAccess keysBucketsStorage API
Free6000Not included
Pro12001025Included
Business6000Unlimited500Included

Pro is 4.99 a month, 49 a year, or 149 once for lifetime. Business is 19.99 a month or 199 a year. Both include storage, transfer, webhooks, presigned URLs and scoped keys; see the S3 page for the storage and transfer allowances that go with them.

  • The ceiling covers the storage surfaces, the S3 gateway and /api/v1 alike. Account and billing endpoints are keyed to your user key and sit outside it.
  • The classic upload API stays free and unchanged. No plan, no key limits, nothing to buy.
  • Webhook deliveries are ours to send and never count against your allowance.

Where next

  • S3 API — endpoint, configuration snippets, operations and error codes.
  • Info / API — the classic upload API and the checkout callback scheme.
  • Account — keys, usage and plans.
  • Erasure coding — how EC 14+4 across 18 storage nodes keeps objects durable.