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

# Authentication

Every authenticated request to the CloudRaker API carries a single header:

```
Authorization: Bearer <token>
```

The gateway accepts two kinds of token under that header and tells them apart automatically:

1. **organization API keys** — the developer story. A machine credential scoped to one organization.
2. **Session JWTs** — what the web app uses for a signed-in human. They carry that person's role and per-resource grants.

Both resolve to the same tenant-scoped identity, so they can call the same routes — with one exception covered under [Capability differences](#api-keys-vs-session-jwts) below.

## Organization API keys

An org API key is a long-lived credential that belongs to your organization, not to a person. Its actions are recorded under the key's name. Use it for server-to-server automation.

### Create a key

Keys are created in the app under **Admin → API keys** (admin-only). The [API keys guide](/admin/api-keys) has the full UI walkthrough. The plaintext value is shown **once** at creation and is never retrievable again.

You can also manage keys over the API (admin-gated):

### Request

GET [https://api.cloudraker.com/api-keys](https://api.cloudraker.com/api-keys)

```curl
curl https://api.cloudraker.com/api-keys \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json"
```

```python
import requests

url = "https://api.cloudraker.com/api-keys"

payload = {}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.get(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.cloudraker.com/api-keys';
const options = {
  method: 'GET',
  headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
  body: '{}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.cloudraker.com/api-keys"

	payload := strings.NewReader("{}")

	req, _ := http.NewRequest("GET", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.cloudraker.com/api-keys")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://api.cloudraker.com/api-keys")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://api.cloudraker.com/api-keys', [
  'body' => '{}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.cloudraker.com/api-keys");
var request = new RestRequest(Method.GET);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.cloudraker.com/api-keys")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Request

POST [https://api.cloudraker.com/api-keys](https://api.cloudraker.com/api-keys)

```curl
curl -X POST https://api.cloudraker.com/api-keys \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
  "name": "CI pipeline"
}'
```

```python
import requests

url = "https://api.cloudraker.com/api-keys"

payload = { "name": "CI pipeline" }
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.cloudraker.com/api-keys';
const options = {
  method: 'POST',
  headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
  body: '{"name":"CI pipeline"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.cloudraker.com/api-keys"

	payload := strings.NewReader("{\n  \"name\": \"CI pipeline\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.cloudraker.com/api-keys")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"name\": \"CI pipeline\"\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.cloudraker.com/api-keys")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"name\": \"CI pipeline\"\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.cloudraker.com/api-keys', [
  'body' => '{
  "name": "CI pipeline"
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.cloudraker.com/api-keys");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"name\": \"CI pipeline\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = ["name": "CI pipeline"] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.cloudraker.com/api-keys")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Request

DELETE [https://api.cloudraker.com/api-keys/\{id}](https://api.cloudraker.com/api-keys/\{id})

```curl
curl -X DELETE https://api.cloudraker.com/api-keys/id \
     -H "Authorization: Bearer <token>"
```

```python
import requests

url = "https://api.cloudraker.com/api-keys/id"

headers = {"Authorization": "Bearer <token>"}

response = requests.delete(url, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.cloudraker.com/api-keys/id';
const options = {method: 'DELETE', headers: {Authorization: 'Bearer <token>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.cloudraker.com/api-keys/id"

	req, _ := http.NewRequest("DELETE", url, nil)

	req.Header.Add("Authorization", "Bearer <token>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.cloudraker.com/api-keys/id")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Delete.new(url)
request["Authorization"] = 'Bearer <token>'

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.delete("https://api.cloudraker.com/api-keys/id")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('DELETE', 'https://api.cloudraker.com/api-keys/id', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.cloudraker.com/api-keys/id");
var request = new RestRequest(Method.DELETE);
request.AddHeader("Authorization", "Bearer <token>");
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["Authorization": "Bearer <token>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.cloudraker.com/api-keys/id")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "DELETE"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

`POST /api-keys` returns the plaintext key value **once**, in the `201` response body. Store it immediately — list and get calls never return it again.

### Use a key

Send it as the bearer token on any request:

### Request

GET [https://api.cloudraker.com/me](https://api.cloudraker.com/me)

```curl
curl https://api.cloudraker.com/me \
     -H "Authorization: Bearer <token>"
```

```python
import requests

url = "https://api.cloudraker.com/me"

headers = {"Authorization": "Bearer <token>"}

response = requests.get(url, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.cloudraker.com/me';
const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.cloudraker.com/me"

	req, _ := http.NewRequest("GET", url, nil)

	req.Header.Add("Authorization", "Bearer <token>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.cloudraker.com/me")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://api.cloudraker.com/me")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://api.cloudraker.com/me', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.cloudraker.com/me");
var request = new RestRequest(Method.GET);
request.AddHeader("Authorization", "Bearer <token>");
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["Authorization": "Bearer <token>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.cloudraker.com/me")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

Validated key identity is cached per gateway instance for \~60 seconds. A revoked key can keep working for up to a minute before the cache clears — factor that into rotation.

## Tenant scoping

One organization is one tenant. After it authenticates you, the gateway derives your tenant from the token's org and scopes **every** call to it — you never send a tenant id, org id, or slug yourself. A token whose identity has no organization gets `404 {"error":"org not found"}`.

## API keys vs. session JWTs

Both token kinds resolve to the same tenant, but they differ on one class of route:

|                               | Organization API key                            | Session JWT                              |
| ----------------------------- | ----------------------------------------------- | ---------------------------------------- |
| Identity                      | The org (a machine credential)                  | A specific human                         |
| Admin / org-level routes      | ✅ Satisfies admin gates                         | ✅ (if the user is an admin)              |
| Per-user resource permissions | ❌ No membership — can't satisfy per-user grants | ✅ Carries the user's per-resource grants |
| Best for                      | Server-to-server, org-wide automation           | The interactive web app                  |

An org key is treated as an org-level machine credential: it passes admin and API-key gates, but it has no user membership, so routes gated on a specific human's fine-grained resource permission can't be satisfied by a key. Org admins bypass per-resource space checks regardless.

## Session JWTs

Session JWTs are three-segment JWTs minted for a signed-in user by the web app's auth flow. They're verified against the platform's signing keys and carry the user's role, permission claims, and membership id. You don't mint these yourself for server-to-server work — reach for an API key instead. They're documented here so you recognize them when the web app or an SDK forwards one.

## Failure codes

| Status | Body                           | Meaning                                                                            |
| ------ | ------------------------------ | ---------------------------------------------------------------------------------- |
| `401`  | `{"error":"unauthorized"}`     | Missing or malformed `Authorization` header                                        |
| `401`  | `{"error":"invalid_token"}`    | Token failed validation                                                            |
| `404`  | `{"error":"org not found"}`    | Token has no associated organization                                               |
| `503`  | `{"error":"auth_unavailable"}` | Transient auth-service outage during key validation — **not** a revoked key; retry |

A `503 auth_unavailable` is deliberately distinct from `401`: during an auth-service outage, a valid key must not look revoked. Treat it as transient and retry with backoff.

## Where to go next

#### [Quickstart](/developers/quickstart)

Make your first authenticated call end to end.

#### [SDKs](/developers/sdks)

Let a typed client set the header for you.