Authentication

🔒

SingleFile uses OAuth 2.0 Client Credentials Flow. You'll exchange a client_id and client_secret for a bearer token, then include that token in every API request.

How It Works

Your App                        SingleFile OAuth
   |                                  |
   |--- POST /o/token/ -------------->|
   |    (client_id + client_secret)   |
   |                                  |
   |<--- access_token (10 hours) -----|
   |                                  |
   |--- GET /external-api/v1/... ---->|  SingleFile API
   |    (Authorization: Bearer token) |
   |                                  |
   |<--- JSON response ---------------|

Step-by-Step

StepActionDetails
1Get credentialsSingleFile provides your client_id and client_secret
2Request tokenPOST to the OAuth endpoint with your credentials
3Make API callsInclude the token in the Authorization header
4Refresh before expiryTokens currently expire after 36000 seconds (10 hours); read expires_in

Get Your Bearer Token

curl -X POST https://api.demo.singlefile.ai/o/token/ \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"
import requests

response = requests.post(
    "https://api.demo.singlefile.ai/o/token/",
    headers={"Content-Type": "application/x-www-form-urlencoded"},
    data={
        "grant_type": "client_credentials",
        "client_id": "YOUR_CLIENT_ID",
        "client_secret": "YOUR_CLIENT_SECRET"
    }
)

access_token = response.json()["access_token"]
const formData = new URLSearchParams({
  grant_type: "client_credentials",
  client_id: "YOUR_CLIENT_ID",
  client_secret: "YOUR_CLIENT_SECRET"
});

const response = await fetch("https://api.demo.singlefile.ai/o/token/", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: formData
});

const { access_token } = await response.json();
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

func main() {
	data := url.Values{
		"grant_type":    {"client_credentials"},
		"client_id":     {"YOUR_CLIENT_ID"},
		"client_secret": {"YOUR_CLIENT_SECRET"},
	}

	resp, err := http.Post(
		"https://api.demo.singlefile.ai/o/token/",
		"application/x-www-form-urlencoded",
		strings.NewReader(data.Encode()),
	)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	var result struct {
		AccessToken string `json:"access_token"`
	}
	json.NewDecoder(resp.Body).Decode(&result)
	fmt.Println("Access token:", result.AccessToken)
}
using System.Net.Http;
using System.Text.Json;

var client = new HttpClient();
var form = new FormUrlEncodedContent(new Dictionary<string, string> {
    ["grant_type"] = "client_credentials",
    ["client_id"] = "YOUR_CLIENT_ID",
    ["client_secret"] = "YOUR_CLIENT_SECRET"
});

var response = await client.PostAsync(
    "https://api.demo.singlefile.ai/o/token/", form);
var json = await response.Content.ReadAsStringAsync();
var accessToken = JsonDocument.Parse(json).RootElement
    .GetProperty("access_token").GetString()!;

Response:

{
  "access_token": "your_bearer_token_here",
  "token_type": "Bearer",
  "expires_in": 36000,
  "scope": "read write"
}

Bad credentials return 401 with the OAuth error format, {"error": "invalid_client"}. The scope value is informational; the API does not use scopes to restrict access.

Token Expiration & Refresh

🕒

Tokens currently expire after 10 hours (expires_in is 36000 seconds). Always read expires_in from the response rather than hard-coding a lifetime, and refresh proactively before expiry. A request made with an expired or invalid token returns 403 with an empty body, not 401, so do not rely on catching a 401 to trigger a refresh.

#!/bin/bash
# Auto-refreshing token helper for shell scripts

CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_SECRET="YOUR_CLIENT_SECRET"
TOKEN_FILE="/tmp/singlefile_token.json"

get_token() {
  local now=$(date +%s)

  # Re-use cached token if still valid (with 60s buffer)
  if [ -f "$TOKEN_FILE" ]; then
    local expires_at=$(jq -r '.expires_at' "$TOKEN_FILE" 2>/dev/null)
    local token=$(jq -r '.access_token' "$TOKEN_FILE" 2>/dev/null)
    if [ -n "$expires_at" ] && [ "$now" -lt "$((expires_at - 60))" ]; then
      echo "$token"
      return
    fi
  fi

  # Fetch a new token
  local response=$(curl -s -X POST https://api.demo.singlefile.ai/o/token/ \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=client_credentials" \
    -d "client_id=${CLIENT_ID}" \
    -d "client_secret=${CLIENT_SECRET}")

  local token=$(echo "$response" | jq -r '.access_token')
  local expires_in=$(echo "$response" | jq -r '.expires_in')
  local expires_at=$((now + expires_in))

  echo "$response" | jq --argjson ea "$expires_at" '. + {expires_at: $ea}' > "$TOKEN_FILE"
  echo "$token"
}

# Usage — always returns a valid token
curl -X GET "https://api.demo.singlefile.ai/external-api/v1/entities/list" \
  -H "Authorization: Bearer $(get_token)" \
  -H "Accept: application/json"
import time
import requests

class SingleFileAuth:
    def __init__(self, client_id, client_secret):
        self.client_id = client_id
        self.client_secret = client_secret
        self.token = None
        self.expires_at = 0

    def get_token(self):
        if self.token and time.time() < self.expires_at - 60:
            return self.token

        response = requests.post(
            "https://api.demo.singlefile.ai/o/token/",
            data={
                "grant_type": "client_credentials",
                "client_id": self.client_id,
                "client_secret": self.client_secret
            }
        )

        data = response.json()
        self.token = data["access_token"]
        self.expires_at = time.time() + data["expires_in"]
        return self.token

auth = SingleFileAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET")

# Always returns a valid token, refreshing automatically
headers = {"Authorization": f"Bearer {auth.get_token()}"}
class SingleFileAuth {
  constructor(clientId, clientSecret) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.token = null;
    this.expiresAt = 0;
  }

  async getToken() {
    if (this.token && Date.now() < this.expiresAt - 60_000) {
      return this.token;
    }

    const response = await fetch("https://api.demo.singlefile.ai/o/token/", {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "client_credentials",
        client_id: this.clientId,
        client_secret: this.clientSecret,
      }),
    });

    const data = await response.json();
    this.token = data.access_token;
    this.expiresAt = Date.now() + data.expires_in * 1000;
    return this.token;
  }
}

const auth = new SingleFileAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET");

// Always returns a valid token, refreshing automatically
const headers = { Authorization: `Bearer ${await auth.getToken()}` };
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"
)

type SingleFileAuth struct {
	ClientID     string
	ClientSecret string
	mu           sync.Mutex
	token        string
	expiresAt    time.Time
}

func (a *SingleFileAuth) GetToken() (string, error) {
	a.mu.Lock()
	defer a.mu.Unlock()

	if a.token != "" && time.Now().Before(a.expiresAt.Add(-60*time.Second)) {
		return a.token, nil
	}

	resp, err := http.PostForm("https://api.demo.singlefile.ai/o/token/", url.Values{
		"grant_type":    {"client_credentials"},
		"client_id":     {a.ClientID},
		"client_secret": {a.ClientSecret},
	})
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	var data struct {
		AccessToken string `json:"access_token"`
		ExpiresIn   int    `json:"expires_in"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
		return "", err
	}

	a.token = data.AccessToken
	a.expiresAt = time.Now().Add(time.Duration(data.ExpiresIn) * time.Second)
	return a.token, nil
}

func main() {
	auth := &SingleFileAuth{
		ClientID:     "YOUR_CLIENT_ID",
		ClientSecret: "YOUR_CLIENT_SECRET",
	}

	// Always returns a valid token, refreshing automatically
	token, _ := auth.GetToken()
	fmt.Println("Authorization: Bearer", token)
}
using System.Net.Http;
using System.Text.Json;

public class SingleFileAuth
{
    private readonly string _clientId;
    private readonly string _clientSecret;
    private readonly HttpClient _http = new();
    private string? _token;
    private DateTimeOffset _expiresAt;

    public SingleFileAuth(string clientId, string clientSecret)
    {
        _clientId = clientId;
        _clientSecret = clientSecret;
    }

    public async Task<string> GetTokenAsync()
    {
        if (_token is not null && DateTimeOffset.UtcNow < _expiresAt.AddSeconds(-60))
            return _token;

        var form = new FormUrlEncodedContent(new Dictionary<string, string> {
            ["grant_type"] = "client_credentials",
            ["client_id"] = _clientId,
            ["client_secret"] = _clientSecret
        });

        var response = await _http.PostAsync(
            "https://api.demo.singlefile.ai/o/token/", form);
        var json = await response.Content.ReadAsStringAsync();
        var doc = JsonDocument.Parse(json).RootElement;

        _token = doc.GetProperty("access_token").GetString()!;
        var expiresIn = doc.GetProperty("expires_in").GetInt32();
        _expiresAt = DateTimeOffset.UtcNow.AddSeconds(expiresIn);
        return _token;
    }
}

// Usage
var auth = new SingleFileAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET");

// Always returns a valid token, refreshing automatically
var token = await auth.GetTokenAsync();
var headers = new Dictionary<string, string> {
    ["Authorization"] = $"Bearer {token}"
};

Security Best Practices

🛡️

Never expose credentials in client-side code, public repos, or logs.

  • Store client_id and client_secret in environment variables or a secrets manager
  • Never log or print bearer tokens in production
  • Use HTTPS for all API requests
  • Rotate credentials periodically; contact SingleFile support to issue a new secret, since there is no self-service rotation endpoint

Did this page help you?