> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://infonite.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://infonite.dev/_mcp/server.

# Authentication & keys

Two different things are protected here, and they use different mechanisms. **Your application** proves who it is with a secret in a header. **The end user's credentials**, which travel inside the request, can be encrypted with a public key so that not even an intercepted body reveals them.

---

## The application secret

Every call carries it:

```http
X-APP-SECRET: <your application secret>
```

**The secret belongs on your server and nowhere else.** It can start executions for any of your customers and read every record extracted under your application. It must never reach a browser, a mobile binary, a front-end bundle or anything a user can open — there is no client-side use of this API, by design.

Secrets are issued per application in the [console](https://www.infonite.tech/console), and an application can hold more than one. That is what makes rotation uneventful: mint the new secret, deploy it, then revoke the old one. Revoking a secret never touches the application, its executions or its data.

**When authentication fails**, the answer is an HTTP status and a stable `detail` code — branch on the code, show your own message:

| Status | `detail`                     | What happened                                     |
| :----- | :--------------------------- | :------------------------------------------------ |
| `401`  | `not_authenticated`          | The header was missing.                           |
| `401`  | `invalid_application_secret` | The value is not a well-formed secret.            |
| `401`  | `unauthorized`               | It is well formed but unknown.                    |
| `401`  | `app_secret_revoked`         | It was valid and has been revoked.                |
| `403`  | `app_pending`                | The application exists but is not approved yet.   |
| `403`  | `app_locked`                 | The application has been locked. Contact support. |
| `403`  | `app_archived`               | The application has been archived.                |

---

## Sandbox and production

**Each application is one environment or the other**, and its secret only works for its own. There is no flag on the request that switches between them: you hold two applications and two secrets.

#### Sandbox

Engines that answer with realistic, fabricated records. No real person and no real institution behind them, so you can run the same case a hundred times — including the ones that fail on purpose.

#### Production

Real engines against real institutions, with a real person's data behind every call. Every execution costs a login against a source, so it is worth arriving here with the journey already rehearsed.

The engine catalogue is environment-aware: [List Engines](api:GET/config/engines/list) on a sandbox application returns the sandbox engines, and only those.

### Base URLs

| Environment                 | Base URL                            |                         |
| :-------------------------- | :---------------------------------- | :---------------------- |
| **Production, today**       | `https://clients.infonite.tech/api` | Build against this one. |
| **Production, coming soon** | `https://clients.infonite.io/api`   | Not serving yet.        |

**`clients.infonite.io` is the address this API is moving to.** It is not available yet; when it is, `clients.infonite.tech` becomes deprecated and is retired after a migration window we will announce here and in the [changelog](/changelog/changelog). Nothing else changes — same paths, same secrets, same payloads — so keeping the host in configuration rather than in code is all the preparation you need.

The explorer beside each endpoint in the reference lets you pick a server, so you can try a call without leaving this documentation.

---

## Encrypting the credentials you send

Everything below is optional — the API accepts plain JSON over TLS — and it is worth doing anyway. TLS protects the connection; this protects the **value**, from the moment it leaves your process until it is used inside ours.

**Each application has an RSA key pair whose private half is generated inside the platform and never leaves it.** You get the public key. Encrypt with it and nobody downstream can read what you sent: not an intermediary, not a proxy log, not your own process afterwards.

That asymmetry is the whole point, and it is why this is the right envelope for somebody else's password: a shared key would mean whoever can encrypt can also decrypt, and both sides holding the same secret is exactly what you do not want here.

#### Get your public key

In the [console](https://www.infonite.tech/console), under your application's keys: a PEM public key, and an X.509 certificate carrying the same key for stacks that expect one. The certificate is self-signed — it is a container with a validity window, not a chain of trust; the key inside it does the work.

#### Encrypt, and say so in the value

An encrypted value is self-describing: the scheme, `::`, and the Base64 of the encrypted bytes.

| Prefix     | Scheme                               | Use it for                                                                          |
| :--------- | :----------------------------------- | :---------------------------------------------------------------------------------- |
| `rsa::`    | RSA, PKCS#1 v1.5                     | One short value. A 2048-bit key caps the payload at roughly 245 bytes.              |
| `hybrid::` | AES-256-GCM under an RSA-wrapped key | **Everything else** — a whole credentials object exceeds the RSA limit immediately. |

#### Send it where a plain value would go

Either field by field, or the whole object as one string. Both are accepted:

**`Field by field`**

```json title="Field by field"
{
  "parameters": {
    "username": "12345678Z",
    "password": "rsa::Q2lwaGVydGV4dEJhc2U2NA=="
  }
}
```

**`The whole object`**

```json title="The whole object"
{
  "parameters": "hybrid::QW5FbmNyeXB0ZWRKU09OT2JqZWN0"
}
```

The same envelope works for [Resume an Execution](api:PUT/executions/handler/v1/\{execution_id}) — which is where one-time codes travel, and where it matters most.

**A payload that cannot be decrypted is rejected at the door, not halfway through.** The call answers `422` naming the field that failed, with `error_decrypting_value` — no execution is created and nothing is charged. It is almost always one of three things: the wrong key, `rsa::` on a payload too large for it, or Base64 mangled in transit.

### A certificate renewal is not a key rotation

Worth knowing before it happens, because the words sound alarming and the event is not. **Reissuing the certificate re-signs the same key pair**: everything you have already encrypted still decrypts, and the certificate you installed keeps working until you choose to replace it. Generating a *new pair* is the destructive one — anything encrypted against the old public key becomes unreadable — which is why it is refused for an application that already has one.

**Two key pairs exist, and confusing them costs an afternoon.** The **inbound** pair is the one on this page: we generate it, we hold the private half, and you encrypt *towards us* with its public key. An **outbound** pair is the mirror — your own public key, whose private half we never hold — used when the platform encrypts something *towards you*. Different direction, different key, different problem.

#### [Payload Encryption](/guides/payload-encryption)

The full cryptography guide — including `aes::`, the scheme that travels in the **other** direction when the platform encrypts what it sends you.

---

## Not storing the credentials at all

The most robust protection is not to hold the value. If your application has tokenization enabled, one execution can hand you a token that stands in for those credentials from then on — scoped to one engine, one customer and one set of features, revocable at any moment.

#### [How tokenization works](/guides/credential-tokenization)

The design: the key derivation, why neither side can open the credentials alone, and what happens if you lose your half.

#### [Stored credentials](/direct-executions/tokenization)

The endpoints: the ticket, the exchange, and what each token status means for your next run.

---

## Every response carries

| Header                                                              | What it is for                                                                                                                           |
| :------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `x-request-id`                                                      | The identifier of that exact call. **Log it.** Quote it to support and we can find the request without asking you to reproduce anything. |
| `x-process-time`                                                    | Seconds we spent processing, excluding the network. Useful for telling *"they are slow"* apart from *"the link is slow"*.                |
| `x-ratelimit-limit` · `x-ratelimit-remaining` · `x-ratelimit-reset` | The ceiling in force for that endpoint, what is left of it, and when the window resets.                                                  |

**Read your budget from the response, do not hardcode it.** A client that watches those headers keeps working when a limit is tuned; one built around numbers copied from a document does not. Exceeding a limit answers `429` with a `detail` naming which limit you hit, so a retry can back off against the right one.