# Getting your credentials

> Only one value is actually secret, and it arrives with the tenant it belongs to. Here is how to ask for it and where each value goes.

Most of what you need is already public. **The API key is the only value you do
not have**, and the tenant id comes with it — they are issued together and they
are bound to each other.

| value | what it is | who has it |
|---|---|---|
| **endpoint** | the asset-manager service URL | **published below** |
| **CDN base** | where bytes are served from | **already the default** — you rarely set it |
| **tenantId** | your numeric tenant | **issued with your key** |
| **API key** | `amk_rt_…` | **the one thing we send you** |

## Requesting access

nitida is in **closed beta** — it currently runs the media for our own products,
and access is granted case by case rather than through a signup form.

**Write to [nitida@gofuture.space](mailto:nitida@gofuture.space)** with what you
are building and roughly how much media you expect. You get back a **tenant
code, a tenant id, and three keys**.

There is no self-serve flow yet, and saying so plainly beats pointing at a form
that does not exist.

## 1 · The endpoint

```
https://api.nitida.gofuture.space
```

That is the real, current production endpoint — not a placeholder. It is public;
every request to it is authenticated by your key.

:::note[If you were given an older address, it still works]
Before 2026-08-17 this page published a long, unbranded host name. **That
address is not going away.** The branded name is an additional mapping onto the
*same* service — same instance, no extra network hop — and the same request
returns a byte-identical body through either name.

So nothing breaks if you are already using it. Point new work at
`api.nitida.gofuture.space` and move the rest whenever it suits you.
:::

In a **browser** context the endpoint may be relative (`/api/media`), which is
how the BFF pattern works: your page calls your own route, and your route holds
the key. Node and Bun always need an absolute URL and throw a clear error
otherwise.

## 2 · The CDN base — you do not need to set this

It defaults to `https://8ok.uk`. Unless someone told you otherwise, **leave it
alone**: no env var, no fallback expression, nothing to configure.

```ts
// This is enough. There is no `?? "https://8ok.uk"` to write.
setTenantId(12);
```

Set it only if you were given a different host.

## 3 · The tenant, and why you cannot pick one

Your tenant is a pair: a code like `"acme"` and a numeric id like `12`.

:::danger[The key decides the tenant. Always.]
The authoritative tenant is `apiKey.metadata.tenantId`, read from the verified
key. The `X-Tenant-Code` header is **ignored** when the key carries a scope —
it exists for logs, not for access.

So **you cannot address a tenant that is not yours** by changing a header or a
body field, and there is nothing to get wrong: send your own code or send
nothing, the answer is the same.

Creating tenants requires a **system** scope, above even a tenant's own admin
key. Customers do not mint tenant ids; we do, when we issue the keys.
:::

The numeric id is what builds URLs, and it is **safe in the browser** — it only
appears in public paths. Ship it as `NEXT_PUBLIC_NITIDA_TENANT_ID`.

⚠️ Variant paths use the id in **base36**: tenant 12 is `/c/v/…`, tenant 10 is
`/a/v/…`. This is invisible for tenants 0–9, where decimal and base36 look
identical, and it is the single most common 404 in this SDK. Never build the
path by hand — call `getAssetUrl` after `setTenantId`.

## 4 · The three keys, and the fourth you ask for

Issued together, per tenant:

| key | ops | who holds it |
|---|---|---|
| **runtime** `amk_rt_*` | read, write, list | your server / BFF |
| **admin** `amk_ad_*` | + delete, admin, upscale | your back-office, never a user-facing app |
| **ci** `amk_ci_*` | + delete, admin | build pipelines |

**A runtime key carries no paid operation, and that is deliberate.** A metered
op bills a third party per call — an upscale run costs €0.10 — so a credential
that lives on a server handling public traffic must not be able to spend money.
Runtime keys used to carry `upscale`, at 12 000 requests per minute; nothing
ever used it.

| key | ops | how you get it |
|---|---|---|
| **metered** `amk_mt_*` | read, write, list, **upscale** | ask for it — not issued by default |

It is rate-limited to 60 requests per minute and cannot `delete`. If you call a
metered route without one, the platform answers `QUOTA_EXCEEDED` with a monthly
limit of zero — which is **not** a rate limit you can wait out. A ceiling of
zero means this credential is not entitled to the operation at all, so retrying
never succeeds. Ask for a metered key.

They are **printed exactly once** — the platform cannot show them to you again.
Save them to your secret manager the moment you receive them. Rotating means
issuing a new set and revoking the old one.

:::danger[The runtime key is a write key to a paid platform]
In a client bundle — including anything named `NEXT_PUBLIC_*` — it is a write
key strangers can read, and your storage bill is what finds out.

The shape that is always correct: **the browser talks to your route, your route
talks to nitida.** The SDK enforces it — `@nitida/sdk/web` exports a client
whose *type* rejects `apiKey`.
:::

## The whole setup

```bash title=".env — server only"
NITIDA_ENDPOINT=https://api.nitida.gofuture.space
NITIDA_RUNTIME_KEY=amk_rt_...
```

```bash title=".env — safe to expose"
NEXT_PUBLIC_NITIDA_TENANT_ID=12
```

```ts title="lib/media.ts — server"
import { NitidaClient } from "@nitida/sdk/server";

export const nt = new NitidaClient({
  endpoint: process.env.NITIDA_ENDPOINT!,
  apiKey: process.env.NITIDA_RUNTIME_KEY!,
  tenantCode: "acme",   // log-only; the key is what actually scopes you
  tenantId: 12,
});
```

```ts title="lib/media-urls.ts — imported anywhere, client components included"
import { setTenantId } from "@nitida/asset-client";

// Process-global: pin it once where your helpers live and every module that
// imports a builder is configured. Forgetting this is the base36 404 above.
setTenantId(Number(process.env.NEXT_PUBLIC_NITIDA_TENANT_ID));
```

That is the entire configuration. Two env vars on the server, one in the client,
and no CDN base to think about.

## Telling a credential problem from something that looks like one

| what you see | what it means |
|---|---|
| `tenant_not_found` | the tenant code has no row, or the 60-second config cache is stale |
| **401 / 403** on presign | wrong key, revoked key, or a runtime key attempting an admin op |
| **404** on a variant URL | usually the base36 prefix or a missing `setTenantId` — **but check `visibility` first**: a `private` asset answers 404 on every public door, and that IS an access decision. See [Access and privacy](/concepts/access-and-privacy/) |
| **400** on a transform URL | not auth either — the width is off the allowed ladder |

The 400 is never a credential problem. The 404 usually is not — but since
private assets shipped it sometimes is, and that case is invisible from the URL,
which is why `visibility` is the first thing to check rather than the last.