Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions authentication.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
title: "Authentication"
description: "How authentication works in the Recoup API — API keys, access tokens, and organization access control."
---

## Overview

Every request to the Recoup API must be authenticated using exactly one of two mechanisms:

| Method | Header | Use case |
|--------|--------|----------|
| API Key | `x-api-key` | Server-to-server integrations |
| Access Token | `Authorization: Bearer <token>` | Frontend apps authenticated via Privy |

Providing both headers in the same request will result in a `401` error.

---

## API Keys

API keys are the primary way to authenticate programmatic access to the Recoup API.

### Creating an API Key

1. Navigate to [chat.recoupable.com/keys](https://chat.recoupable.com/keys)
2. Enter a descriptive name (e.g. `"Production Server"`)
3. Click **Create API Key**

<Warning>
Copy your API key immediately — it is only shown once. Keys are stored as a secure HMAC-SHA256 hash and cannot be retrieved after creation.
</Warning>

### Using an API Key

Pass your key in the `x-api-key` header:

```bash
curl -X GET "https://api.recoupable.com/api/tasks" \
-H "x-api-key: YOUR_API_KEY"
```

---

## Access Tokens (Privy)

If you're building a frontend application that authenticates users via [Privy](https://privy.io), you can pass the user's Privy JWT as a Bearer token instead of an API key.

```bash
curl -X GET "https://api.recoupable.com/api/tasks" \
-H "Authorization: Bearer YOUR_PRIVY_JWT"
```

The API validates the token against Privy, extracts the user's email, and resolves it to the corresponding Recoup account. Bearer tokens always authenticate as a personal account — they cannot act on behalf of an organization.

---

## Personal vs. Organization API Keys

API keys inherit the type of account they were created under.

### Personal API Keys

- Created by a standard user account
- Can only access **your own account's data**
- `orgId` is `null` on the resolved auth context

### Organization API Keys

- Created by an organization account (an account that has members)
- Can access data for **any member account** within the organization
- `orgId` is set to the organization's account ID

<Info>
An account is recognized as an organization when it has at least one member in the `account_organization_ids` table.
</Info>

---

## How We Determine Key Type at Creation

When a key is created under an account, the API checks whether that account has any organization members:

```
Has members in account_organization_ids?
├── Yes → Organization API Key (orgId = accountId)
└── No → Personal API Key (orgId = null)
```

This check happens at **authentication time** (not creation time), so key behavior automatically reflects the current state of the account.

Comment on lines +79 to +90
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Section title contradicts behavior timing.

Line 79 says key type is determined “at Creation,” but Line 89 says the check happens at authentication time. Please align the title and copy so the behavior is unambiguous.

Suggested wording update
-## How We Determine Key Type at Creation
+## How Key Type Is Resolved
@@
-When a key is created under an account, the API checks whether that account has any organization members:
+When a key is used, the API checks whether the owning account currently has any organization members:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## How We Determine Key Type at Creation
When a key is created under an account, the API checks whether that account has any organization members:
```
Has members in account_organization_ids?
├── Yes → Organization API Key (orgId = accountId)
└── No → Personal API Key (orgId = null)
```
This check happens at **authentication time** (not creation time), so key behavior automatically reflects the current state of the account.
## How Key Type Is Resolved
When a key is used, the API checks whether the owning account currently has any organization members:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@authentication.mdx` around lines 79 - 90, The section title "How We Determine
Key Type at Creation" contradicts the copy that states the check happens at
authentication time; rename the title to "How We Determine Key Type at
Authentication" and update the surrounding text to consistently state that the
system checks "Has members in account_organization_ids?" during authentication
(not at creation), so that the decision logic (Organization API Key vs Personal
API Key) and the note "this check happens at authentication time" are aligned
and unambiguous.

---

## How We Verify Access on API Calls

Every authenticated request goes through `validateAuthContext`, which enforces the following access rules:

### Personal API Key or Bearer Token

Can only access their **own account**. Attempting to pass an `account_id` belonging to another account returns `403 Forbidden`.

### Organization API Key

Can access **any account that is a member of the organization**:

```
Request includes account_id override?
├── Same as key owner → Allowed (self-access)
├── Is a member of the org → Allowed
└── Not a member → 403 Forbidden
```

Membership is verified by querying the `account_organization_ids` table for a record linking the target account to the organization.

<Note>
The Recoup internal admin organization has universal access to all accounts.
</Note>

### Organization Access via `organization_id`

Some endpoints accept an `organization_id` parameter. When provided, the API additionally validates that the authenticated account is either:

- A **member** of the organization, or
- The **organization account itself**

---

## Error Responses

| Status | Cause |
|--------|-------|
| `401` | Missing or invalid credentials, or both `x-api-key` and `Authorization` headers provided |
| `403` | Valid credentials but insufficient access to the requested `account_id` or `organization_id` |

---

## Security Notes

- API keys are **never stored in plaintext** — only an HMAC-SHA256 hash (keyed with your project secret) is persisted in the database
- **Never include `account_id` in your API key creation request** — the account is always derived from your authenticated credentials
- Rotate keys immediately if compromised via the [API Keys Management Page](https://chat.recoupable.com/keys)
2 changes: 2 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"index",
"quickstart",
"mcp",
"sdk",
"authentication",
"cli",
"api-reference/sandboxes/create"
]
Expand Down