# Your account

> Keys, people, usage, requests and purchases over the API.

**TL;DR**: everything about your account lives under `/v1/account` on
`https://api.megalania.io`: balance, keys, people, usage, request records, purchases,
top-ups and the referral program. Sign in with Google or GitHub, make an account key
(`meg_ak_…`) on the Keys page, and send it in the `Authorization` header.
Only `GET /v1/account` also takes your scrape key.

Every example reads `MEG_ACCOUNT_KEY` from the environment. [Get a key](/start/get-a-key) shows
where the key comes from. The [account reference](/reference/account) lists
every route, generated from the API contract.

## Keys and scopes

A key has one scope, chosen when it is made. The prefix shows it: `meg_sk_` is a scrape key, `meg_ak_` is an account key. The rest is 43 random characters. We store only a hash and the last 4 characters, so we can never show a key again.


| scope | works on | does not work on |
| -- | -- | -- |
| `scrape` | `/v1` (scraping), batches, `GET /v1/account` | the rest of `/v1/account`: 403 `wrong_scope` |
| `account` | everything under `/v1/account` | scraping, batches, proxy: 403 `wrong_scope` |

- **Header only.** Send `Authorization: Bearer $MEG_ACCOUNT_KEY`. The
  `api_key` query parameter is refused here with 401 `bad_token`, because a
  key in a URL ends up in logs and browser history.
- **A revoked key stops working in about 1 second.** A new key works within about a second.
- **Every answer carries `Cache-Control: no-store`** and `X-Request-Id`.
  There are no CORS headers: call this API from your server, never from a
  browser page.
- **Every write takes JSON.** Send `Content-Type: application/json`, even
  with an empty `{}` body. Bodies are at most 4 KiB. A key the request does
  not take is a 400 `bad_parameter` naming it.
- **10 requests a second per key**, bursts of 20. Above that: 429
  `rate_limited` with `Retry-After`.

## Roles

A person in the account is an `owner` or a `member`.

| what | owner | member |
| -- | -- | -- |
| read the account, usage, the member list | yes | yes (no emails) |
| make a key for themselves | yes | yes |
| list and revoke keys | every key | their own keys |
| read request records | every request | requests made with their own keys |
| add people, change roles | yes | no: 403 `owner_only` |
| remove a person | anyone | only themselves (leave) |
| read purchases, top up | yes | no: 403 `owner_only` |
| the referral program | yes | no: 403 `owner_only` |

The account always keeps at least one owner: 409 `last_owner` otherwise.

## Balance and limits

```sh
curl -s -H "Authorization: Bearer $MEG_ACCOUNT_KEY" "https://api.megalania.io/v1/account"
```

```json
{
  "id": "0199b0a1-0000-7000-8000-00000000a001",
  "created_at": "2026-09-23T10:00:00Z",
  "balance": 98750,
  "held": 150,
  "grant": null,
  "plan": { "name": "prepaid", "concurrency": 5, "max_members": 10, "max_keys": 20 },
  "caps": {
    "daily_cost_cap":  { "reached": false, "resets_at": "2026-09-24T00:00:00Z" },
    "period_cost_cap": { "reached": false, "resets_at": null },
    "free_failures":   { "used_percent": 12, "reached": false, "resets_at": "2026-09-24T00:00:00Z" },
    "spend_cap":       { "cap": 50000, "used": 12500, "used_percent": 25, "reached": false, "resets_at": "2026-10-01T00:00:00Z" }
  },
  "default_country": "DE",
  "found_us": "friend",
  "country_in_use": "DE",
  "premium_credits": 3,
  "alerts": { "webhook_url": "https://hooks.example.com/megalania", "email": false, "email_available": false },
  "webhook_secret": { "set": true, "rotated_at": "2026-09-20T08:00:00Z", "previous_valid_until": null },
  "subscription": null,
  "caller": { "person_id": "0199b0a1-...", "role": "owner", "key_id": "0199b0a1-...", "scope": "account" }
}
```

- `balance`: credits you can spend now. `held`: credits reserved by requests
  still running. They go back to `balance`, or are charged, when those finish.
- `grant`: the part of `balance` from a free grant, with its end date. `null`
  when there is none.
- `plan`: your limits. `caps`: whether a usage limit is reached, named like
  the 429 you would get, and when it resets. `free_failures`: how much of
  today's free browser and residential retries you used, in percent; at
  `reached` such requests answer 429 `free_failures_spent` until 00:00 UTC.
- `default_country`: the country requests without `country_code` exit from,
  set by an owner. `null` means the platform default. `country_in_use` is the
  country used now; it differs when we no longer serve your choice. See
  [Countries](/guides/countries).
- `premium_credits`: the extra credits per request on a premium country.
- `found_us`: how an owner said the account found us: `search`,
  `ai_assistant`, `friend`, `social`, `community`, `directory`, `blog` or
  `other`. `null` until they answer. Set it once with
  `PATCH /v1/account {"found_us": "friend"}`; the first answer is kept and a
  later one is ignored. It is optional, and only an owner sets it.
- `caps.spend_cap`: your monthly spend cap and this UTC month's use against
  it, named like the 402 you would get. See [Spending cap](#spending-cap).
- `alerts`: where usage alerts go, and whether we can send email yet. See
  [Usage alerts](#usage-alerts).
- `webhook_secret`: whether a secret exists and when it was rotated. Never the
  secret itself. See [Webhook secret](#webhook-secret).
- `subscription`: your latest subscription (`plan`, `status`, `period_end`,
  `cancel_at_period_end`), or `null` when you never subscribed.
- `caller`: who you are: your `person_id`, your role, the key you sent.

It is free. `$MEG_API_KEY` works here too.

## Keys

List them. An owner sees every key, a member sees their own:

```sh
curl -s -H "Authorization: Bearer $MEG_ACCOUNT_KEY" "https://api.megalania.io/v1/account/keys"
```

The list gives each key's `kind` (`api`, `mcp` or `dashboard`) and `hint` (its last 4 characters). `mcp` marks a key made by an MCP connection (Claude, ChatGPT). The list never holds the key itself.

Make one. `expires_at` is required, in the future and at most 400 days
ahead. `name` is 1 to 64 characters, default the scope word. `scope` is
`scrape` (the default) or `account`:

```sh
curl -s -X POST "https://api.megalania.io/v1/account/keys" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "laptop", "scope": "scrape", "expires_at": "2027-01-01T00:00:00Z"}'
```

The answer is 201 with `api_key` (a `meg_sk_...` or `meg_ak_...` string), shown once, and `key`, its record. A key
always belongs to the person who made it. When that person leaves the
account, their keys are revoked. Too many keys is 402 `over_plan`.
A new key works within about 1 second. A script that uses it right away
waits 1 second first, or retries a 401 once after 1 second.

Revoke one. It stops working in about 1 second:

```sh
export KEY_ID='<id from the list>'
curl -s -X DELETE -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  "https://api.megalania.io/v1/account/keys/$KEY_ID"
```

The answer is the key as it was. A key you cannot see is 404 `not_found`.

## People

Members are API only in the beta: the dashboard has no Members page yet.

List them. Owners see emails; for members `email` is `null`:

```sh
curl -s -H "Authorization: Bearer $MEG_ACCOUNT_KEY" "https://api.megalania.io/v1/account/members"
```

Add one (owner). `role` is `member` (the default) or `owner`. We send no
email:

```sh
curl -s -X POST "https://api.megalania.io/v1/account/members" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "ada@example.com"}'
```

The answer is 201 with the new `person_id`. An email that is already in this
account is 409 `already_member`. Too many people is 402 `over_plan`.

The new member signs in with Google or GitHub with that email and lands in this
account. They make their own keys on the Keys page. Never send a key by chat
or email.

Change a role (owner):

```sh
export PERSON_ID='<person_id from the list>'
curl -s -X PATCH "https://api.megalania.io/v1/account/members/$PERSON_ID" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"role": "owner"}'
```

Remove someone (owner), or leave (your own `person_id`):

```sh
curl -s -X DELETE -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  "https://api.megalania.io/v1/account/members/$PERSON_ID"
```

```json
{ "person_id": "0199b0a1-...", "keys_revoked": 2, "items_canceled": 40 }
```

Removing a person revokes every key they made and cancels their batch items
that have not started, in one step.

## Usage

Requests and credits per UTC day, newest first. `days` is 1 to 90, default
30. Today is included and still growing:

```sh
curl -s -H "Authorization: Bearer $MEG_ACCOUNT_KEY" "https://api.megalania.io/v1/account/usage?days=7"
```

```json
{ "days": 7, "usage": [
  { "date": "2026-09-23", "requests": 1200, "delivered": 1100, "blocked": 60, "credits_charged": 5400, "credits_refunded": 40,
    "by_source": { "api": { "requests": 1000, "credits": 4600 }, "mcp": { "requests": 150, "credits": 600 }, "job": { "requests": 50, "credits": 200 } } }
] }
```

`credits_charged` is what you paid that day, already net: a refunded request
adds 0 to it. `credits_refunded` is for information; do not subtract it again.
`by_source` splits the day: `job` is a batch item, `mcp` a request made with a
key from an MCP sign-in (Claude, ChatGPT), `api` everything else.

## Request records

One record per request, newest first, the last 90 days. `limit` is 1 to 100,
default 50. `key_id` keeps one key's requests; `source` (`api`, `mcp`, `job`
or `monitor`) keeps one source's, and `api` also matches `proxy`; `host` (like
`example.com`, any case) keeps one site's. With any filter the list covers the
last 7 days, not 90:

```sh
curl -s -H "Authorization: Bearer $MEG_ACCOUNT_KEY" "https://api.megalania.io/v1/account/requests?limit=50"
```

Each record has `id` (the `X-Request-Id` you got), `created_at`, `key_id`,
`key_name` (`null` once the key is deleted), `source` (`api`, `proxy`, `job`,
`monitor` or `mcp`), `client` (`claude` or `chatgpt` when `source` is `mcp`), `job_id`,
`monitor_id` (the monitor when `source` is `monitor`),
`engine`, `target_status`,
`error_code` (a code from [errors.md](/reference/errors), `null` when delivered),
`error_message` (the one-line message for that code, the same text the API
answered with; absent when delivered),
`blocked`, `challenge`, `credits_charged`, `duration_ms`, `bytes` and
`target_host`: the site's host name only, like `example.com`. We never store
the path, the query or the full URL. It is `null` when your account keeps no
hosts (audit level `minimal`).

A record is `mcp` while its key exists. Once an MCP key is deleted (unused for
30 days, expired, or replayed), its records show as `api` here. The usage
totals keep them as `mcp` once their hour is rolled up, about an hour after the
hour ends, because the rollup fixes the source.

### One request

`GET /v1/account/requests/{id}` gives one record, by the `X-Request-Id` you
got, with more detail: the climb and where the credits came from.

```sh
curl -s -H "Authorization: Bearer $MEG_ACCOUNT_KEY" "https://api.megalania.io/v1/account/requests/<request id>"
```

```json
{"id": "...", "credits_charged": 12, "route": "h:dc>c:dc",
 "hops": [{"rung": "h:dc", "identity": "firefox", "ms": 310, "outcome": "blocked"},
          {"rung": "c:dc", "identity": "chrome", "ms": 1200, "outcome": "ok"}],
 "credit_parts": {"route": 9, "output": 2, "other": 1},
 "llm_tokens": 0, "response_types": ["markdown", "json"], "...": "the list fields"}
```

- `hops` is each route we tried, in order, the same as the `X-Hops` header.
  Every try before the last was not delivered and cost nothing. It is `null`
  when no route was tried.
- `credit_parts` is the same split as the `X-Credits-Route`,
  `X-Credits-Output` and `X-Credits-Other` headers
  ([Credits](/guides/credits#where-the-credits-went)). `route + output + other` is
  `credits_charged`. It is `null` for requests from before we kept the parts.
- `llm_tokens` is the language-model tokens the request used, and
  `response_types` the outputs you asked for.
- A member sees only the requests of their own keys. Any other id is a 404,
  never a 403. The dashboard reads the same record at
  `GET /api/account/requests/{id}`.

Every list here pages the same way: send `next_cursor` back as `cursor` until
it is `null`.

```sh
curl -s -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  "https://api.megalania.io/v1/account/requests?limit=50&cursor=<next_cursor>"
```

**What we keep about your requests.** For each request we keep a record: its id, time, key, engine, result, credits and the target's host name (like shop.example.com). We never write the full target URL, its path or query, your request headers, cookies, or your IP address into a log or a table. A batch is the one exception you choose: it needs its URLs to run them, so it keeps them while it runs and deletes them 7 days after it ends (see [Batches](/guides/batches)). GET /v1/account/requests shows you your records.

## Purchases

Owner only. Paid purchases, newest first:

```sh
curl -s -H "Authorization: Bearer $MEG_ACCOUNT_KEY" "https://api.megalania.io/v1/account/purchases"
```

```json
{ "purchases": [
  { "id": "0199b0a1-...", "created_at": "2026-09-23T10:00:00Z", "credits": 100000,
    "amount": "25.00", "currency": "USD", "status": "paid", "paid_by": null,
    "campaign": "reddit-launch", "receipt_url": null }
], "next_cursor": null }
```

`amount` is a decimal string. `paid_by` is the person who bought, `null`
once that person has left the account. `receipt_url` is always
`null` today.

## Spending cap

Owner only. A cap is the most credits this account may spend in one UTC
month. Requests, batch items, monitor checks and LLM steps all count, as
charged, plus what requests still running have reserved.

```sh
curl -s -X PATCH "https://api.megalania.io/v1/account" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"spend_cap": 50000}' | jq .caps.spend_cap
```

```json
{ "cap": 50000, "used": 12500, "used_percent": 25, "reached": false, "resets_at": "2026-11-01T00:00:00Z" }
```

- A request that would take this month past the cap answers `402 spend_cap`.
  It costs nothing, and the message says when it resets. A batch item ends
  `failed` with the same error; a monitor check is skipped for free.
- The cap resets at 00:00 UTC on the 1st. Send `"spend_cap": null` to remove
  it. A member can read the cap but not change it.
- The cap counts credits, not money: it is the same number you see in
  `X-Credits-Charged`.
- The dashboard sets it on **Plan and billing**, and shows a banner at 80%
  and 100%.

## Usage alerts

We tell you at 50, 80 and 100% of your spend cap. With no cap set, we count
against your last grant or top-up instead. Each step is sent once. We also
tell you once when a request is stopped: by the cap, or, with no cap, because
the balance could not cover it.

```sh
curl -s -X PATCH "https://api.megalania.io/v1/account" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"alerts": {"webhook_url": "https://hooks.example.com/megalania"}}'
```

- `webhook_url` gets a signed `POST`, like the batch webhook (same secret,
  same headers, `X-Megalania-Event: account.usage`). It must be `https` on a
  public host. `null` removes it. A failed delivery is tried again after 1 s,
  5 s, 25 s and 125 s.
- `email: true` mails every owner whose email is verified. Email is not
  available yet: `alerts.email_available` is `false`, and `email: true` is
  refused with `400 bad_parameter` until it is.

```json
{
  "event": "account.usage",
  "kind": "used",
  "used_percent": 80,
  "base": "spend_cap",
  "spend_cap": 50000,
  "resets_at": "2026-11-01T00:00:00Z",
  "created_at": "2026-10-22T09:14:03Z"
}
```

- `kind` is `used` (a step was reached) or `stopped` (a request was refused;
  `used_percent` is the real use before it).
- `base` is `spend_cap` or `grant`. The body then carries `spend_cap` (the
  cap) or `grant` (the credits of the last grant). `resets_at` is `null` for a
  grant.
- The body names no host and no URL.

## Webhook secret

Every webhook we send you (batches, monitors, usage alerts) is signed with one
secret per account. We show it once, when it is made, and never again:

```sh
curl -s -X POST "https://api.megalania.io/v1/account/webhook-secret/rotate" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY"
```

```json
{ "secret": "bmV3LXNlY3JldC1ieXRlcy0wMTIzNDU2Nzg5YWJjZGVmZ2g=", "previous_valid_until": "2026-10-23T09:14:03Z" }
```

- Owner only, with an account key, or on the dashboard: Connect, Webhooks,
  **Rotate and show**.
- Store it in your secret manager at once. Lost it? Rotate again.
- For 24 hours the old secret also signs: the signature header carries both
  values, newest first. Update your receivers in that window. A second
  rotation inside it drops the oldest secret.
- Leaked? Rotate twice. The second rotation ends the leaked secret at once,
  instead of after 24 hours.
- At most 5 rotations an hour.
- How to check a signature: [Batches, check the signature](/guides/batches#check-the-signature).

## Webhooks

Send a signed `test` event to check your receiver:

```sh
curl -s -X POST "https://api.megalania.io/v1/account/webhooks/test" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY" -H "Content-Type: application/json" \
  -d '{}'
```

```json
{ "status": 200, "error": null, "duration_ms": 87 }
```

- `{}` sends it to your `alerts.webhook_url`. To try another receiver, send
  `{"url": "<https URL>"}`. It must be one https URL on a public host.
- The body is `{"id", "event": "test", "created_at"}`, signed like every
  webhook, with `X-Megalania-Event: test`. We send it once, with no retry.
- The answer is what your receiver said: `status` is its HTTP status, `null`
  when it did not answer. Then `error` says why: `timeout`, `connect`,
  `private` (the host is not on the public internet) or `request`.
- It is free. At most 5 a minute per account.
- Every member can send a test; only an owner rotates.

## Subscribe

Owner only. `GET /v1/plans` lists the plans (no key needed). The first one,
`free`, is the free tier: its `price` and `currency` are `null`, and it
needs no subscription. Subscribe to one of the others:

```sh
curl -s -X POST "https://api.megalania.io/v1/account/subscription" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"plan": "starter"}' | jq -r .checkout_url
```

Open the printed link and pay. Each paid month adds the plan's monthly
credits; they last 61 days. With a subscription, the same call changes the
plan: a higher plan is charged in full now and starts a new month with its
full credits (`{"change": "upgrade"}`); a lower plan starts at your next
renewal (`{"change": "downgrade"}`). Your card, your invoices and canceling
are on the billing portal: `POST /v1/account/billing-portal` returns
`portal_url`. A cancel takes effect at the end of the paid month, and your
credits stay until they expire.

## Top up

Owner only. A pack tops up an active subscription and is sold only for your
plan; without a subscription the answer is `409 no_subscription`. It opens a
Stripe payment page whose credits land on this account. The dashboard's
**Plan and billing** page does the same, under Top up. It makes no keys:

```sh
curl -s -X POST "https://api.megalania.io/v1/account/checkout" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"pack": "starter"}' | jq -r .checkout_url
```

Open the printed link in a browser and pay. The credits show in `balance`
once Stripe confirms the payment.

A body may carry `ref_token` (from a referral link) or `ref_code` (a code
someone gave you). Only the account's **first paid purchase** binds a
referral; a ref on a later purchase is dropped. A bad ref is never an
error.

## Referral program

Owner only. Share your link; when an account you referred
pays real money, you earn **12% of what it paid, as credits**, never cash.

- Your account can join after one paid purchase that was not refunded or
  disputed. Otherwise: 403 `not_eligible`.
- Earnings wait **60 days** after each payment, then are paid as credits at
  the list price once a month.
- It counts the referred account's purchases in its first 12 months, up to
  $250 per referred account.
- Nothing is earned on your own account, an account that shares a person
  with yours, an account whose people already belong to another account, a
  free account, or a click from an ad. We match people by their sign-in
  email, never by the email typed into Stripe.

Join. It makes your first code. Sending it again changes nothing:

```sh
curl -s -X POST "https://api.megalania.io/v1/account/affiliate" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  -H "Content-Type: application/json" -d '{}'
```

Your link is `link` in the answer: `https://api.megalania.io/r/<code>`. Add
`?campaign=<name>` to see which post it came from, and `to=/buy` to send
people straight to the Buy credits page. The referral binds when the
referred account makes its first paid purchase, after it signs in.

Read your status, terms, codes and earnings:

```sh
curl -s -H "Authorization: Bearer $MEG_ACCOUNT_KEY" "https://api.megalania.io/v1/account/affiliate"
```

Make another code (at most 5):

```sh
curl -s -X POST "https://api.megalania.io/v1/account/affiliate/codes" \
  -H "Authorization: Bearer $MEG_ACCOUNT_KEY" \
  -H "Content-Type: application/json" -d '{}'
```

Clicks, referrals and earnings per month, `months` 1 to 12, default 6:

```sh
curl -s -H "Authorization: Bearer $MEG_ACCOUNT_KEY" "https://api.megalania.io/v1/account/affiliate/stats?months=3"
```

Before you join, these three answer 404 `not_enrolled`.

## Errors

| HTTP | `error.code` | what to do |
| -- | -- | -- |
| 400 | `bad_parameter` | Read `error.field` and the message. |
| 400 | `bad_request` | Send one JSON object. |
| 401 | `bad_token` | Send your key as `Authorization: Bearer <key>`. Not in the query. |
| 402 | `over_plan` | Revoke a key or remove a person, then try again. |
| 402 | `spend_cap` | Your monthly spend cap is reached. Raise it with `PATCH /v1/account`, or wait for the 1st (UTC). Nothing was charged. |
| 403 | `wrong_scope` | Use your account key, not your scrape key. |
| 403 | `owner_only` | Ask an owner. |
| 404 | `not_found` | No key, person or pack with that id or name in this account. |
| 409 | `already_member` | The email is already here. Find its `person_id` in the list. |
| 409 | `last_owner` | Make someone else an owner first. |
| 429 | `rate_limited` | Wait the seconds in `Retry-After`. |
| 503 | `busy` | Send the same request again in a few seconds. |

The referral codes `not_eligible` and `not_enrolled`, and every other code
with its cost, are in [errors.md](/reference/errors).
