API Reference

Create disposable aliases, decide who reaches your inbox, and check whether mail arrived. REST over HTTPS, JSON in and out.

Base URLhttps://api.ghstmail.space/v1
One thing up front. GhstMail never stores the content of your mail, so no endpoint returns a message, a subject, or a sender. What you get is alias configuration and delivery counters. If mail was received and forwarded, it is in your real inbox. That is the whole design, not a missing feature.

Everything you can do

WhatAPICLI
Create an aliasPOST /v1/aliasesghstmail new [label]
List aliasesGET /v1/aliasesghstmail alias list
Read one aliasGET /v1/aliases/{id}ghstmail alias get <ref>
Rename, or set an expiryPATCH /v1/aliases/{id}ghstmail alias update <ref>
Stop delivery, keep the addressPATCH … {"active": false}ghstmail alias disable <ref>
Delete an aliasDELETE /v1/aliases/{id}ghstmail alias rm <ref>
Block a sender by domainPOST /v1/filtersghstmail filter add <domain>
List or remove filtersGET, DELETE /v1/filtersghstmail filter list, rm
Send mail as an aliasPOST /v1/messagesghstmail send
Did mail arrive and forward?GET /v1/aliases/{id}ghstmail check <ref>
Watch delivery liveGET /v1/aliases (ETag)ghstmail watch [ref]
Account and usage totalsGET /v1/accountghstmail account
Inspect or revoke your keyGET, DELETE /v1/keys/currentghstmail whoami, logout
Log in without pasting a keyPOST /v1/device/authorizeghstmail login
Check the service is healthyGET /v1/health, /v1/readyghstmail doctor

There is no endpoint that returns a received message, a subject, or a sender. Nothing is stored, so there is nothing to return.

Quickstart

Create a key in the dashboard, export it, and make your first alias. Two minutes, no SDK.

bash
curl https://api.ghstmail.space/v1/aliases \
  -H "Authorization: Bearer $GHSTMAIL_KEY" \
  -d '{"label": "newsletter"}'

You get the alias back. Mail sent to it lands in your real inbox.

json
{
  "object": "alias",
  "id": "9c1f5e02-4a7b-4c3d-8e21-5b6a7c8d9e0f",
  "address": "newsletter.k7f2q@ghstmail.space",
  "label": "newsletter",
  "active": true,
  "expires_at": null,
  "emails_received": 0,
  "emails_forwarded": 0,
  "created_at": "2026-07-29T10:24:11.028Z",
  "updated_at": "2026-07-29T10:24:11.028Z"
}

Note the omitted Content-Type: curl infers it from -d. Every other client should send application/json explicitly, because anything else gets a 415.

Authentication

One API key in a bearer header. Keys look like gm_live_… and are shown once, at creation. We store only a hash, so a lost key is replaced rather than recovered.

http
Authorization: Bearer gm_live_kQ8xR2mN...

Keys carry scopes, and the dashboard lets you narrow them. A key that only reads aliases cannot delete one, which makes it safe to drop into a script you do not fully trust.

aliases:readList and retrieve aliases
aliases:writeCreate, update and delete aliases
filters:readList filters
filters:writeCreate and delete filters
messages:sendSend mail from your aliases. Never granted by default.

Keep keys server-side. CORS is deliberately not enabled, so a browser cannot call this API. That is a guardrail, not an oversight: a key in front-end JavaScript is a key you have published.

Errors

Every failure has the same shape, so you write the handler once. The request_id also appears in our logs, so quote it if you need help.

json
{
  "error": {
    "type": "validation_error",
    "code": "parameter_invalid",
    "message": "'domain': must match pattern",
    "param": "domain"
  },
  "request_id": "req_0850ce41f4fe1f98cf0990b9"
}
400The request itself does not make sense
401Missing, malformed, revoked or expired key
403The key lacks the scope for this call
404No such alias, filter, or route
409Address taken, or a key is mid-flight
422A parameter failed validation, see param
429Slow down, see Retry-After
5xxOur fault. Safe to retry

Pagination

List endpoints return newest first, 20 at a time, up to 100. Page with starting_after, passing the id of the last object you saw. Cursors rather than offsets, so creating an alias mid-loop does not make you skip or repeat one.

bash
curl "https://api.ghstmail.space/v1/aliases?limit=20&starting_after=$LAST_ID" \
  -H "Authorization: Bearer $GHSTMAIL_KEY"
json
{
  "object": "list",
  "data": [ { "object": "alias", "...": "..." } ],
  "has_more": true
}

Keep going while has_more is true. There is a complete loop in the Python example below.

Rate limits

120 requests a minute per account. Every response tells you where you stand, so you never have to guess.

http
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1785320760

X-RateLimit-Reset is a Unix timestamp. Go over and you get a 429 with Retry-After in seconds. Back off when Remaining gets low rather than after you are refused.

GET /v1/aliases also returns an ETag. Send it back as If-None-Match and an unchanged list costs you a 304 with no body, which is how the CLI can poll every five seconds politely.

Idempotency

Send an Idempotency-Key on any POST and a retry is free of consequences. If the first attempt already succeeded you get that same response back, marked Idempotent-Replayed: true, rather than a duplicate alias.

bash
curl https://api.ghstmail.space/v1/aliases \
  -H "Authorization: Bearer $GHSTMAIL_KEY" \
  -H "Idempotency-Key: 7f3c1a90-signup-form" \
  -d '{"label": "signup"}'

Any unique string of 8 to 255 characters works; a UUID is the easy choice. Reuse a key with a different body and you get a 422 rather than a surprise, because that is a bug rather than a retry. Keys are remembered for 24 hours.

Aliases

GET/v1/aliasesaliases:read
POST/v1/aliasesaliases:write
GET/v1/aliases/{id}aliases:read
PATCH/v1/aliases/{id}aliases:write
DELETE/v1/aliases/{id}aliases:write

Every field on create is optional. A label is a note to yourself and also seeds the address, which makes aliases recognisable months later.

bash
# Self-destructs in 24 hours
curl https://api.ghstmail.space/v1/aliases \
  -H "Authorization: Bearer $GHSTMAIL_KEY" \
  -d '{"label": "free trial", "expires_in": 86400}'

# Or choose the address yourself
curl https://api.ghstmail.space/v1/aliases \
  -H "Authorization: Bearer $GHSTMAIL_KEY" \
  -d '{"local_part": "shopping"}'

Use expires_in for seconds from now, or expires_at for an exact time. Sending both is an error rather than a coin toss.

To stop mail without giving up the address, set active to false. Reach for this before DELETE, which is permanent and takes the reply tokens with it, so old threads can no longer reach you.

bash
curl -X PATCH https://api.ghstmail.space/v1/aliases/$ALIAS_ID \
  -H "Authorization: Bearer $GHSTMAIL_KEY" \
  -d '{"active": false}'

emails_received and emails_forwarded are the honest answer to "did it arrive?". A gap between them means a filter caught something.

Filters

GET/v1/filtersfilters:read
POST/v1/filtersfilters:write
DELETE/v1/filters/{id}filters:write

Filters match on the sender's domain. Leave alias_id out and the rule covers every alias you own; set it and the rule is limited to one.

bash
# Stop everything from one domain, across every alias
curl https://api.ghstmail.space/v1/filters \
  -H "Authorization: Bearer $GHSTMAIL_KEY" \
  -d '{"domain": "spam.example.com"}'

# Or scope it to a single alias
curl https://api.ghstmail.space/v1/filters \
  -H "Authorization: Bearer $GHSTMAIL_KEY" \
  -d '{"domain": "marketing.example.com", "alias_id": "'$ALIAS_ID'"}'

Blocked mail is counted in emails_received and then dropped, so you can see that something was stopped without it reaching you.

Sending mail

POST/v1/messagesmessages:send

Send as one of your aliases. The recipient sees the alias, never your real address, and their reply comes back through the alias to your inbox. The message is DKIM-signed as ghstmail.space.

bash
curl https://api.ghstmail.space/v1/messages \
  -H "Authorization: Bearer $GHSTMAIL_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "from": "newsletter.k7f2q@ghstmail.space",
    "to": "someone@example.com",
    "subject": "Following up",
    "text": "Sent from an alias. Reply and it reaches me."
  }'

from takes an alias id or address and is resolved server-side against the aliases you own. There is deliberately no way to supply a raw From header, so a key cannot be used to claim an address that is not yours.

json
{
  "object": "message",
  "id": "<8b8c988c-2347-0a4a-a0c0-72fa77cb76f4@ghstmail.space>",
  "from": "newsletter.k7f2q@ghstmail.space",
  "to": ["someone@example.com"],
  "subject": "Following up",
  "status": "sent",
  "accepted": ["someone@example.com"],
  "rejected": [],
  "quota": {
    "per_hour": { "used": 1, "limit": 60 },
    "per_day": { "used": 1, "limit": 300 }
  },
  "sent_at": "2026-07-29T11:34:02.117Z"
}

You get 202 once the recipient's server accepts the message, with accepted and rejected so partial delivery is visible. Send an Idempotency-Key and a retry cannot send twice.

Limits, and why
  • Scope: needs messages:send, which is not in the default set. No existing key can send.
  • 5 recipients per message, all on one domain. Send separate messages for separate domains.
  • No recipients on ghstmail.space, which would loop back into the alias machinery.
  • Carriage returns and newlines are rejected in the subject, so a caller cannot append their own headers.
  • The alias must be active and unexpired. A burned alias is not a usable sending identity.
  • Quotas per hour and per day, per account and per alias. Exceeding them returns 429 with Retry-After.

Only the recipient's domain, a count and a timestamp are recorded, for quota accounting. Not the subject, not the body, not the recipient's local part. A full recipient log would be a social graph, which is the thing this product exists to avoid.

From the CLI:

bash
# Body inline, from a file, from stdin, or in your editor
ghstmail send --from newsletter --to someone@example.com \
  --subject "Following up" --text "Sent from an alias."

ghstmail send --from newsletter --to someone@example.com \
  --subject "Report" --body ./report.txt

cat notes.md | ghstmail send --from newsletter --to a@example.com \
  --subject "Notes" --body -

# See exactly what would go on the wire, send nothing
ghstmail send --from newsletter --to a@example.com \
  --subject "hi" --text "hello" --dry-run

--dry-run is worth the habit. Nothing about a sent message is stored, so it is the only opportunity to read a message back.

Account

GET/v1/account
GET/v1/keys/current
DELETE/v1/keys/current
GET/v1/health
GET/v1/ready

/v1/account confirms which account a key belongs to and totals your alias counts, which is the cheapest way to check a key works. DELETE /v1/keys/current lets a key retire itself, which is what the CLI does on logout.

/v1/health is liveness and touches nothing. /v1/ready checks the database and queue, and returns 503 when either is unhappy. Point a monitor at the second one.

From your language

There is no SDK to install, and for an API this size that is a feature. Here is the whole thing in JavaScript.

javascript
const API = "https://api.ghstmail.space/v1";

async function createAlias(label) {
  const res = await fetch(`${API}/aliases`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.GHSTMAIL_KEY}`,
      "Content-Type": "application/json",
      // Safe to retry on a dropped connection: you get the
      // original alias back instead of a second one.
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({ label }),
  });

  if (!res.ok) {
    const { error, request_id } = await res.json();
    throw new Error(`${error.code}: ${error.message} (${request_id})`);
  }
  return res.json();
}

const alias = await createAlias("hacker news");
console.log(alias.address);

And in Python, including a pagination loop you can lift as-is.

python
import os, uuid, requests

API = "https://api.ghstmail.space/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['GHSTMAIL_KEY']}"

def create_alias(label: str) -> dict:
    res = session.post(
        f"{API}/aliases",
        json={"label": label},
        headers={"Idempotency-Key": str(uuid.uuid4())},
        timeout=30,
    )
    if not res.ok:
        body = res.json()
        raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
    return res.json()

def iter_aliases():
    """Walk every page without tripping the rate limit."""
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["starting_after"] = cursor
        page = session.get(f"{API}/aliases", params=params, timeout=30).json()
        yield from page["data"]
        if not page["has_more"]:
            return
        cursor = page["data"][-1]["id"]

print(create_alias("hacker news")["address"])

For anything generated, the OpenAPI 3.1 document describes every endpoint.

Command line

The CLI is the fastest way to use any of this. Zero runtime dependencies, and login hands you off to the dashboard to approve, so no key ever touches your shell history.

bash
npm install -g ghstmail
ghstmail login

ghstmail login shows a short code, you approve it while signed in, and the CLI stores the key it is given at mode 0600. Revoke it any time from the dashboard.

bash
# The one you will actually use: create an alias and copy it
$ ghstmail new github
github.k7f2q@ghstmail.space
  copied to clipboard  ·  label: github  ·  never expires

# Throwaway that cleans itself up
$ ghstmail new "free trial" --expires 24h

# Did the signup email arrive?
$ ghstmail check github
github.k7f2q@ghstmail.space
  state      active
  received   1
  forwarded  1
  filters    none

# Block a sender across every alias
$ ghstmail filter add spam.example.com

# Pipe-friendly: stdout is only ever the data
$ ghstmail alias list --json | jq -r '.data[] | select(.active) | .address'

Data goes to stdout and everything decorative goes to stderr, so EMAIL=$(ghstmail new) gives you exactly an address. Exit codes are distinct too: 3 means log in again, 4 a missing scope, 8 rate limited, 10 network. Enough for a wrapper script to do the right thing.

watch is the closest thing to an inbox we can honestly offer. It tails delivery counters, so you can see mail land without us keeping any of it.

bash
$ ghstmail watch github
Watching github.k7f2q@ghstmail.space every 5s. Ctrl-C to stop.

19:04:11  github.k7f2q@ghstmail.space  received +1  forwarded +1  delivered
19:06:40  github.k7f2q@ghstmail.space  received +1  forwarded +0  not forwarded, check filters

ghstmail --help lists everything, and ghstmail doctor is the first thing to run when something looks wrong.