> For the full page index, fetch https://echonova.ai/llms.txt

# Security best practices

How to keep an API key from leaking, limit what a leaked key can do, replace a key safely, trust only genuine webhooks, and audit what your keys did.

An API key is a bearer token: whoever holds it can spend your workspace's
credits until you disable or delete it, and the API cannot tell your server
from an attacker who copied the key. The controls below exist so that a key
is hard to leak, does little damage if it leaks, and is easy to replace. The
same thinking applies to a webhook, which is an endpoint anyone can post to
once they know its URL.

## Keep the key where only your server can read it

The API checks the `Authorization` header and nothing else: no origin, no
IP address, no client certificate. A key that reaches a browser bundle, a
mobile app, a desktop build or a public repository is a key anyone can
extract and use from anywhere. Keep it on a server you control, read it from
an environment variable or a secrets manager at start-up, and have the
server call the API on behalf of your clients. The secret cannot be
recovered after creation, by you or by anyone else: the portal shows it
once and afterwards displays only the first 8 characters.

**Recommended**

- A backend route of your own that your app calls; the route holds the key
  and calls the API.

**Less effective**

- The key in a mobile app's configuration, calling the API directly from the
  device.

> [!WARNING]
> Never send an API key to a user's device, and never commit one to source
> control. If a key has been exposed, disable it in the portal at once,
> then create a replacement. A disabled key is rejected on the next request.

## Give each system its own key with only the access it needs

The portal lets you shape a key when you create or **Edit** it:

| Control          | What it limits                                                                                                                                  | What happens outside the limit                                                                                                                            |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Services**     | Which of Text to Speech and Speech to Text the key may call. Grant **Access** to the ones the system needs and leave the rest on **No Access**. | `403` with `This API key does not have access to Text-to-Speech services` or the Speech to Text equivalent.                                               |
| **Credit Limit** | How many credits the key may spend per billing cycle.                                                                                           | Text to Speech calls are rejected with `429` and `Monthly spending limit exhausted` before they run. Speech to Text calls run and are counted afterwards. |
| The row switch   | Whether the key is accepted at all.                                                                                                             | `401` on every request until you enable it again.                                                                                                         |

Create one key per system that calls the API, named after that system.
When one leaks you disable that one, and the others keep working. When the
request log shows a call you do not recognise, the key name tells you which
system made it. The portal will not save a key without at least one service,
so grant exactly what the system needs. A key made before this change with
no service selected can still call every service, so open it and choose its
services.

## Cap the damage with a credit limit

A credit limit is the only control that bounds spend on a key before you
notice a problem. Set it on every key that runs unattended, at a level a
little above the system's normal monthly use. Then subscribe a webhook to
the two spending events, `spending.warning` at 80% of the limit and
`spending.exhausted` at the limit, so that an unexpected surge reaches you
as a message rather than as an invoice. Both events carry the key's id;
[Webhook events](/docs/developers/webhooks-events) has the bodies. The
count resets on the key's first metered call of a new billing cycle;
see [Credits](/docs/developers/credits#credit-limit-per-key) for the
arithmetic.

## Replace a key instead of hoping it is still secret

A key's secret is fixed at creation. To replace it, create a new key, deploy
it, then delete the old one:

| Step   | What to do                                                                                                               |
| ------ | ------------------------------------------------------------------------------------------------------------------------ |
| Create | Create a new key with the same services and credit limit. Copy or download the secret; it is shown once.                 |
| Deploy | Put the new secret in your server's configuration and restart or reload so it is used.                                   |
| Verify | Make one request with the new key and confirm `200`. The old key's **Last Used** date stops moving once nothing uses it. |
| Delete | **Delete** the old key. Deleting cannot be undone, and requests with it get `401` from then on.                          |

Do this on a schedule you can keep, and at once when a person who knew a
secret leaves or a machine that held one is decommissioned. If you are
unsure whether a key leaked, disable it first with the row switch, which is
reversible, and decide about deletion afterwards. The request log keeps a
deleted key's entries, so nothing is lost for audit.

## Trust a webhook only after you verify it

Your webhook endpoint is public. Anyone who learns its URL can post a JSON
body that looks like `job.completed` with an `audioUrl` of their choosing,
or a `spending.exhausted` that makes you disable a key. EchoNova signs every
delivery with the webhook's secret over the timestamp and the body, so a
forged or altered request fails the check. Verify before you act, compare in
constant time, and reject a timestamp far from your clock; the steps are on
[Verify a webhook signature](/docs/developers/webhooks-verify-signature).

| Fact                                                                | Why it matters                                                                                                                          |
| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| The signing secret is returned once, at creation.                   | Store it with the same care as an API key. To get a new one, create a second webhook and delete the first, as on the verification page. |
| The portal accepts `https` endpoint URLs only.                      | Keep a valid TLS certificate on the endpoint. The body and the headers, including the signature, always cross the network encrypted.    |
| A retry carries a fresh timestamp and signature but the same body.  | De-duplicate on the content, not on the signature.                                                                                      |
| Deliveries do not come from a fixed, published set of IP addresses. | Do not rely on an IP allowlist for authenticity; rely on the signature.                                                                 |
| Only the status code of your response matters.                      | Answer `2xx` once the signature checks out and process afterwards.                                                                      |

## Audit what your keys did

Every request whose API key is found is recorded with its method, path,
status, key and time, and the portal shows the most recent ones on the
**Request Log** tab. Use it to answer "did our system make this call?" and
to spot a key being used from a system that should not have it. A `403` and the
first per-key `429` of each limit window are in the log; a `401`, the per-IP
`429` and later per-key `429`s in the same window are not. The
log holds no request or response body and no header, and it is read in the
portal only. Keep your own log of every request your server makes, with the
key name and the `jobId` it received, so that the two can be matched. The
page [Read the request log](/docs/developers/request-logs) explains each
column.

The workspace owner can create, edit and delete keys and webhooks and read
the log. Anyone who can create a key can spend your credits, so protect the
owner's sign-in with two-factor authentication.

## FAQ

### A key leaked. What do I do first?

Turn it off with the row switch under **Developers**, **API Keys**. The
next request with it gets `401`. Then create a replacement, deploy it, and
delete the old key.

### Can I restrict a key to my server's IP address?

No. The API checks the `Authorization` header only. Keep the key on your
server and put a credit limit on it.

### Can I get the secret back if I lose it?

No. Create a new key and delete the old one.

### Can I rotate a webhook secret in place?

No. Create a second webhook for the same URL and events, deploy its secret,
then delete the first. Your endpoint must accept both secrets during the
overlap.

### Who in my workspace can create keys?

The workspace owner, whose role has `developer_api.manage`. A key spends
your credits, so keep the owner's account secure.

## Related

- [Get an API key](/docs/developers/get-an-api-key)
- [Authentication](/docs/developers/authentication)
- [Credits](/docs/developers/credits)
- [Configure a webhook](/docs/developers/webhooks-configure)
- [Verify a webhook signature](/docs/developers/webhooks-verify-signature)
- [Delivery, retries and failures](/docs/developers/webhooks-retries)
- [Read the request log](/docs/developers/request-logs)
- [Latency best practices](/docs/developers/best-practices-latency)
