SeeDropHost

HTTP API

Checked against the code on 2026-10-31 (version 0.1.0) · Markdown version

Base URL: your server, for example http://localhost:8787 in development. Machine-readable spec: /openapi.json (OpenAPI 3.1).

Authentication

Every route needs Authorization: Bearer <token>, except GET /v1/health, POST /v1/drop and the Asaas webhook (see Billing). There are two kinds of token:

  • Account token (sdh_…): created with POST /v1/account/tokens, up to 20 active per account. It sees and changes only the sites of its own account. A site, deploy or token of another account answers 404, the same as one that does not exist.
  • Server token (SDH_TOKEN): the administrator. It sees every account, is the only one that can create accounts and change plans, and picks the account on account routes with ?conta=<id> (default: the principal account, where sites created before accounts live).

These are tokens with labels, not people: there are no user identities or roles yet.

Optional header x-sdh-actor: <name> (up to 64 characters) sets who appears in the audit log; the default is api.

Errors look like this (messages are in Brazilian Portuguese today). The ones you will meet most:

Statuserror.codeWhen
401unauthorizedtoken missing, invalid or revoked
402plan_limitcreating (or claiming) a site beyond the plan's site count; error.detail has plano and limite. Nothing that is live is touched
403admin_onlythe route is for the server administrator
404not_found and friendsdoes not exist, or belongs to another account
409token_limit, anonymous_site20 active tokens already; export of an anonymous site
{ "error": { "code": "unauthorized", "message": "token ausente ou inválido" } }
export SDH_API_URL=http://localhost:8787
export SDH_TOKEN=dev-token
AUTH="Authorization: Bearer $SDH_TOKEN"   # the administrator
# account routes below also work with an account token (sdh_…):
export ACCOUNT_TOKEN=sdh_…

Health

GET /v1/health

No token. Version and a database check; 503 if the database does not answer.

curl "$SDH_API_URL/v1/health"
# {"ok":true,"service":"seedrophost","version":"0.1.0","db":"ok"}

Sites

GET /v1/sites

An account token lists only its own sites. The administrator sees all of them, or one account with ?conta=.

curl -H "$AUTH" "$SDH_API_URL/v1/sites"

POST /v1/sites

Body {"name": "..."}. 201 with the site, created in the account of the token (the administrator may add "conta": "<id>"; default principal). Beyond the plan's site count the answer is 402 plan_limit. Reserved names: www, api, app, admin, edge, mcp, status, docs, mail, and anything starting with site-, dpl-, acc- or tok- (they look like internal ids).

curl -X POST -H "$AUTH" -H "content-type: application/json" -d '{"name":"my-site"}' "$SDH_API_URL/v1/sites"

GET /v1/sites/{site}

{site} is the name or the id. Adds url and domains.

curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site"

DELETE /v1/sites/{site}

Takes the site down at once and deletes it with its deploys, domains, forms and leads; files no other deploy uses are removed from storage. Answer 200: {"deleted": "my-site"}. Recorded in the audit log as site.delete. There is no undo.

curl -X DELETE -H "$AUTH" "$SDH_API_URL/v1/sites/my-site"

Deploys in one step

The request body is the file itself. A zip of a repository root with netlify.toml / seedrop.toml is laid out like the CLI does: only the publish folder goes live, with the config and functions kept private. Query options: draft=1 (do not publish), title= (up to 200 characters), source= (up to 32 characters). Answer 201: {deploy, urls, files, uploaded, bytes}, where deploy.report is the SEO/GEO report.

POST /v1/sites/{site}/deploys/zip

curl --data-binary @site.zip -H "content-type: application/zip" -H "$AUTH" \
  "$SDH_API_URL/v1/sites/my-site/deploys/zip?title=first"

POST /v1/sites/{site}/deploys/html

The body becomes /index.html.

curl --data-binary @index.html -H "content-type: text/html" -H "$AUTH" \
  "$SDH_API_URL/v1/sites/my-site/deploys/html?draft=1"

Deploys by manifest (only changed files upload)

This is what the CLI uses for folders.

POST /v1/sites/{site}/deploys

Body: files maps each public path to the SHA-256 (hex) of its content; optional draft, title, source, git_ref. Answer 201: {deploy, required, urls}. required lists the hashes the server does not have yet; when it is empty, the deploy is already ready.

H=$(shasum -a 256 index.html | cut -d' ' -f1)
curl -X POST -H "$AUTH" -H "content-type: application/json" \
  -d "{\"files\":{\"/index.html\":\"$H\"},\"draft\":true,\"title\":\"manifest\"}" \
  "$SDH_API_URL/v1/sites/my-site/deploys"

PUT /v1/deploys/{id}/blobs/{hash}

Upload one missing file; the server checks that the content matches the hash.

curl -X PUT --data-binary @index.html -H "$AUTH" "$SDH_API_URL/v1/deploys/dpl-…/blobs/$H"
# {"ok":true}

POST /v1/deploys/{id}/finalize

Closes the deploy after the uploads, builds the report and, if it is not a draft, publishes it. 409 missing_files lists what is still missing.

curl -X POST -H "$AUTH" "$SDH_API_URL/v1/deploys/dpl-…/finalize"

GET /v1/sites/{site}/deploys

Latest first. limit from 1 to 100 (default 20). Each item has live, url and score.

curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/deploys?limit=5"

GET /v1/deploys/{id}

The deploy with its full report (SEO/GEO, forms, rules).

curl -H "$AUTH" "$SDH_API_URL/v1/deploys/dpl-…"

POST /v1/sites/{site}/publish

Body {"deploy_id": "..."}. Puts a ready deploy live: approves a draft or rolls back. Takes effect on the next request. The draft field of a deploy keeps recording how it was created; live in the deploy list says what is being served.

curl -X POST -H "$AUTH" -H "content-type: application/json" -H "x-sdh-actor: release-bot" \
  -d '{"deploy_id":"dpl-…"}' "$SDH_API_URL/v1/sites/my-site/publish"

Publishing without an account

POST /v1/drop

No token. Only when the server runs with SDH_ANON_DROP=1 (otherwise 404). Body: a zip (content-type: application/zip) or an HTML file (content-type: text/html), up to 25 MiB; a zip may hold up to 50 MiB uncompressed. Answer 201: url, expires_at (24 h), claim_token (shown once), site, deploy_id.

Refusals, each with a Retry-After header:

Statuserror.codeWhen
429rate_limitedmore than 5 per network per hour (IPv4 address or IPv6 /64; Retry-After: 3600), or the server-wide hourly cap was reached (Retry-After: 600)
503busy2 drops are already being processed (Retry-After: 10)
503drop_quotastorage for drops is full (Retry-After: 3600)
413too_largebody above 25 MiB
curl --data-binary @site.zip -H "content-type: application/zip" "$SDH_API_URL/v1/drop"

POST /v1/drop/claim

With a token. Body {"claim_token": "...", "name": "optional-new-name"}. Removes the expiry and the anonymous restrictions, and the site joins the account of the token (and counts toward its site limit: 402 plan_limit when the plan is full).

curl -X POST -H "$AUTH" -H "content-type: application/json" \
  -d '{"claim_token":"ab0e…","name":"my-site"}' "$SDH_API_URL/v1/drop/claim"

Environment variables

Values are encrypted at rest (AES-256-GCM). Secret values are always masked in answers, for example s3c••••••ue.

GET /v1/sites/{site}/env

curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/env"

PUT /v1/sites/{site}/env/{key}

Key: UPPER_CASE, digits and _, up to 128 characters. Body: value; optional secret (boolean), scopes (build, functions, runtime), contexts (production, preview).

curl -X PUT -H "$AUTH" -H "content-type: application/json" \
  -d '{"value":"…","secret":true}' "$SDH_API_URL/v1/sites/my-site/env/API_KEY"

DELETE /v1/sites/{site}/env/{key}

curl -X DELETE -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/env/API_KEY"

Custom domains

A custom domain is only served after its TXT record is verified.

GET /v1/sites/{site}/domains

curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/domains"

POST /v1/sites/{site}/domains

Body {"hostname": "www.example.com"}. Answer 201 with the DNS records to create: a TXT _seedrop.<hostname> and a CNAME (or A records for an apex domain).

curl -X POST -H "$AUTH" -H "content-type: application/json" \
  -d '{"hostname":"www.example.com"}' "$SDH_API_URL/v1/sites/my-site/domains"

POST /v1/domains/{hostname}/verify

Looks up DNS and answers verified, routed, what was found and the next step.

curl -X POST -H "$AUTH" "$SDH_API_URL/v1/domains/www.example.com/verify"

Forms and leads

GET /v1/sites/{site}/forms

Forms detected in the deploys, with fields, number of submissions, spam blocked and last submission.

curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/forms"

GET /v1/sites/{site}/submissions

Latest first. Query: form, spam=1 (include spam), limit 1–500 (default 50).

curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/submissions?form=contact&limit=20"

Audit

GET /v1/audit

Latest first, limit 1–500 (default 50). Each entry: actor, action, target, detail_json, at. An account token sees only the entries of its own account; the administrator sees all, or one account with ?conta=.

curl -H "$AUTH" "$SDH_API_URL/v1/audit?limit=20"

Accounts (administrator only)

These three routes answer 403 admin_only to an account token.

POST /v1/accounts

Body: nome (required, up to 100 characters), optional plano (gratis, pro or agencia; default gratis) and rotulo (label of the first token). Answer 201: {conta, token, token_id, aviso}. The token is shown once; the server keeps only a SHA-256 of it.

curl -X POST -H "$AUTH" -H "content-type: application/json" \
  -d '{"nome":"Client Ltda","plano":"gratis","rotulo":"first token"}' "$SDH_API_URL/v1/accounts"

GET /v1/accounts

Every account with its sites count.

curl -H "$AUTH" "$SDH_API_URL/v1/accounts"

PATCH /v1/accounts/{conta}

Body with any of plano, nome, pagamento_falhou_em (ISO date, or null when the payment is back). The plan change takes effect at once. With billing on, the server sets pagamento_falhou_em by itself when an invoice is overdue and clears it when the invoice is paid. Answers 404 for an unknown account and 400 for an invalid plan or date.

curl -X PATCH -H "$AUTH" -H "content-type: application/json" \
  -d '{"plano":"pro"}' "$SDH_API_URL/v1/accounts/acc-…"

Your account, usage and tokens

With an account token these routes act on the token's own account. The administrator adds ?conta=<id> (default principal).

GET /v1/account

The account (id, nome, plano, dates of the warning and plan-change request, pagamento_falhou_em), plus admin (true when the caller used the server token) and the number of sites.

curl -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account"

GET /v1/account/usage

Plan, limits, usage of the current month and what to do next (texts in Brazilian Portuguese). Sites, bandwidth and leads are counted; deploys never count. Going over bandwidth or leads only changes estado to aviso, and two months in a row to pedir_troca (a plan change is requested, with 14 days): no site goes offline and every lead is kept (leads_acima_do_plano says how many are above the plan). Other fields: plano, limites, historico (two previous months), acima_neste_mes, meses_seguidos_acima, pedir_troca, pagamento, mensagens, proximos_passos.

curl -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account/usage"
# shortened:
# {"conta":{"nome":"Client Ltda","plano":"gratis"},"mes":"2026-10",
#  "limites":{"sites":3,"banda_gb_mes":30,"leads_mes":100,"deploys":"ilimitados"},
#  "uso":{"sites":1,"banda_gb":0.05,"leads":4,"leads_acima_do_plano":0,"deploys":6},
#  "estado":"ok","proximos_passos":["nada a fazer: a conta está dentro do plano. Deploys são ilimitados."]}

POST /v1/account/tokens

Body: optional rotulo (label, up to 100 characters). Answer 201: {id, rotulo, criado_em, token, aviso}. The token (sdh_…) is shown once. Give one token to each machine or agent. At most 20 active tokens per account: the 21st answers 409 token_limit; revoke one first.

curl -X POST -H "Authorization: Bearer $ACCOUNT_TOKEN" -H "content-type: application/json" \
  -d '{"rotulo":"ci"}' "$SDH_API_URL/v1/account/tokens"

GET /v1/account/tokens

The tokens of the account without their secret: id, rotulo, criado_em, ultimo_uso_em, revogado_em. Revoked tokens stay in the list for 90 days.

curl -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account/tokens"

DELETE /v1/account/tokens/{token}

{token} is the token id (for example tok-…), not the secret. The token stops working at once. A token of another account answers 404 token_not_found.

curl -X DELETE -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account/tokens/tok-…"

Billing

The plan subscription is paid by Pix or boleto on the invoice page of Asaas, the payment provider; a card also works on that page and never passes through the server. Billing is off until the administrator sets SDH_ASAAS_API_KEY. The price always comes from the plan table on the server: a value sent by the client is ignored. CPF, CNPJ and email go to Asaas and are not stored on the server. Nothing here takes a site offline or deletes anything: when an invoice is overdue the sites stay live, and only after 30 days does creating a new site follow the Free limits. The hosted plans open with the first public server; there is no date.

GET /v1/account/billing

estado (gratis, pendente, ativo, atrasado or cancelado), plano, ciclo, valor, pago_ate, pagamento_falhou_em, proxima_cobranca, fatura_em_aberto (url, vencimento, estado em_aberto or vencida, valor), troca_pendente (a change waiting for payment), troca_agendada (a move down and when it starts), precos, mensagens and proximos_passos. cobranca_ligada is false when billing is off on the server. While a payment is awaited, the server reads Asaas again, waiting longer each time (30 s up to 1 h) and for at most 24 h. CLI: sdh plano; MCP: get_billing.

curl -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account/billing"
# shortened:
# {"cobranca_ligada":true,"estado":"ativo","plano":"pro","ciclo":"mensal","valor":29,
#  "pago_ate":"2026-11-30T02:59:59.000Z","proxima_cobranca":{"data":"2026-11-30T02:59:59.000Z","valor":29},
#  "fatura_em_aberto":null,"troca_pendente":null,"troca_agendada":null,"mensagens":["plano Pro pago até 29/11/2026."]}

POST /v1/account/billing/checkout

Body: plano (pro or agencia), ciclo (mensal or anual; default mensal), nome (who pays), cpfCnpj (check digits are validated before calling Asaas) and email. Send an Idempotency-Key header, for example a UUID: repeating it, or asking again for the same plan and cycle, returns the same subscription, and an account never has two open subscriptions. Answer 201: plano, ciclo, valor, efeito, invoiceUrl, pix (qr as a base64 PNG, copiaecola, expira_em) when the invoice has Pix, vencimento and proximo_passo. CLI: sdh assinar pro --nome "…" --cpf … --email ….

efeito says when the new plan takes effect:

efeitoWhen
apos_pagamentofrom Free, or with no paid time left: when the invoice is paid
agoramoving up (a bigger plan, or the same plan in a cycle that costs more per day): at once; the paid time left becomes credito_dias days of the new plan, and its first charge is due after them
agendadamoving down: the current plan, already paid, lasts until pago_ate (inicia_em), then the new one starts. The old subscription stops charging now; nothing is refunded or charged twice
retomadathe same plan again after a cancel: charged again at the end of the paid period

Refusals: 400 (invalid_plan, invalid_cycle, invalid_name, invalid_cpf_cnpj, invalid_email), 409 already_on_plan, 409 payment_just_confirmed (the waiting payment arrived first: check GET /v1/account/billing before asking again), 409 scheduled_paid, 429 checkout_limit (more than 5 attempts in 24 h) and 503 billing_off.

curl -X POST -H "Authorization: Bearer $ACCOUNT_TOKEN" -H "content-type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"plano":"pro","ciclo":"mensal","nome":"Client Ltda","cpfCnpj":"11.222.333/0001-81","email":"billing@example.com"}' \
  "$SDH_API_URL/v1/account/billing/checkout"

POST /v1/account/billing/cancel

Stops charging now. The plan lasts until pago_ate and then the account goes back to Free; nothing is deleted and no site goes offline. Before cancelling, the server reads Asaas again, so a payment that just arrived is never thrown away. Answer 200 with the same body as GET /v1/account/billing; 409 no_subscription when there is nothing to cancel. CLI: sdh cancelar-assinatura --sim.

curl -X POST -H "Authorization: Bearer $ACCOUNT_TOKEN" "$SDH_API_URL/v1/account/billing/cancel"

GET /v1/admin/billing

Administrator only. What is missing to charge: chave (the Asaas key: ok, falta, recusada or sem_resposta), token_webhook, host_do_painel, webhook (cadastrado, interrompido, desligado or falta), pronto, falta and proximo_passo. Changes nothing. CLI: sdh cobranca.

curl -H "$AUTH" "$SDH_API_URL/v1/admin/billing"

POST /v1/admin/billing/webhook

Administrator only. Body: optional email, which gets the Asaas warnings when the webhook fails. Registers only the SeeDropHost webhook (https://<SDH_PANEL_HOST>/v1/billing/asaas/webhook) or turns its paused queue back on; other webhooks of the Asaas account are never touched. CLI: sdh cobranca:webhook --email you@example.com.

curl -X POST -H "$AUTH" -H "content-type: application/json" \
  -d '{"email":"you@example.com"}' "$SDH_API_URL/v1/admin/billing/webhook"

POST /v1/billing/asaas/webhook

Called by Asaas, not by clients. No Bearer: the asaas-access-token header must match SDH_ASAAS_WEBHOOK_TOKEN (401 otherwise; 404 when billing is off). Once the token is accepted the answer is always 200, so Asaas never pauses the queue: events of other systems on the same Asaas account are ignored and each event is processed once. The event is never trusted: the server reads the payment or subscription again on Asaas before changing anything.

Take everything (export)

GET /v1/sites/{site}/export

A zip with site/ (the files of the live deploy, including _redirects, _headers and netlify.toml / seedrop.toml), leads.csv (every submission, with a spam column; cells that start with =, +, - or @ get an apostrophe so they never run as formulas), variaveis.txt (the names of the variables only: values never leave, not even masked) and LEIA-ME.txt. With ?resumo=1 the answer is JSON with the numbers (arquivos, leads, variaveis, dominios) and the download path instead of the zip. An anonymous drop answers 409 anonymous_site: claim it first.

curl -H "$AUTH" -o my-site.zip "$SDH_API_URL/v1/sites/my-site/export"
curl -H "$AUTH" "$SDH_API_URL/v1/sites/my-site/export?resumo=1"

Internal route

GET /v1/internal/tls-check

Not for clients. Caddy asks it before issuing a certificate (?domain=). It only answers calls from the machine itself without proxy headers; anything else gets 403, and the shipped Caddyfile answers 404 for /v1/internal/* from outside.

MCP endpoint

POST /mcp speaks MCP (Streamable HTTP, JSON-RPC) with the same Bearer token. See MCP for agents.