Skip to main content
The Factus API uses a two-layer authentication system to provide secure access to invoice operations. You need both a local JWT token and a Factus access token to make API calls.

Authentication Layers

The API implements two distinct authentication layers:
  1. Local JWT Authentication: Protects all API endpoints with a local access token
  2. Factus Token Authentication: Required for operations that interact with the Factus service
This two-layer approach ensures that only authenticated users can access your API, while the Factus token provides secure communication with the external Factus service.

Quick Start

1

Get Local JWT Token

First, authenticate with the local API to receive a JWT access token.
curl -X POST "http://localhost:8000/api/v1/auth/login" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=admin&password=admin123"
Response:
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}
The default credentials are admin / admin123. In production, these should be replaced with proper user authentication.
2

Get Factus Access Token

Use your local JWT token to authenticate with Factus and receive a Factus access token.
curl -X POST "http://localhost:8000/api/v1/auth/factus/login" \
  -H "Authorization: Bearer YOUR_LOCAL_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "your-factus-password"
  }'
Response:
{
  "success": true,
  "message": "Autenticación exitosa",
  "data": {
    "access_token": "factus_token_here",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "factus_refresh_token_here"
  }
}
3

Make Authenticated Requests

Now you can make requests to protected endpoints using both tokens:
curl -X POST "http://localhost:8000/api/v1/invoices" \
  -H "Authorization: Bearer YOUR_LOCAL_JWT_TOKEN" \
  -H "X-Factus-Token: YOUR_FACTUS_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d @invoice.json

Required Headers

All protected endpoints require specific headers:

For All Protected Endpoints

Authorization: Bearer YOUR_LOCAL_JWT_TOKEN
This header is required for all API endpoints except /auth/login.

For Factus Operations

Authorization: Bearer YOUR_LOCAL_JWT_TOKEN
X-Factus-Token: YOUR_FACTUS_ACCESS_TOKEN
Invoice and lookup endpoints require both the Authorization header and the X-Factus-Token header.

Token Refresh Workflow

Factus access tokens expire after a certain period (typically 1 hour). Use the refresh token to obtain a new access token without re-entering credentials.
1

Check Token Expiration

The Factus login response includes an expires_in field indicating the token lifetime in seconds.
{
  "access_token": "...",
  "expires_in": 3600,
  "refresh_token": "..."
}
2

Refresh the Token

Before the token expires, use the refresh token to get a new access token:
curl -X POST "http://localhost:8000/api/v1/auth/factus/refresh" \
  -H "Authorization: Bearer YOUR_LOCAL_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "YOUR_FACTUS_REFRESH_TOKEN"
  }'
Response:
{
  "success": true,
  "message": "Token refrescado exitosamente",
  "data": {
    "access_token": "new_factus_token_here",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "new_refresh_token_here"
  }
}
3

Update Your Token

Replace your old Factus access token with the new one in subsequent requests.
Always store tokens securely and never expose them in client-side code or version control.

Code Examples

Python

import httpx
from typing import Optional

class FactusClient:
    def __init__(self, base_url: str):
        self.base_url = base_url
        self.local_token: Optional[str] = None
        self.factus_token: Optional[str] = None
        self.refresh_token: Optional[str] = None

    async def login_local(self, username: str, password: str):
        """Authenticate with the local API"""
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.base_url}/api/v1/auth/login",
                data={"username": username, "password": password}
            )
            response.raise_for_status()
            data = response.json()
            self.local_token = data["access_token"]

    async def login_factus(self, email: str, password: str):
        """Authenticate with Factus"""
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.base_url}/api/v1/auth/factus/login",
                headers={"Authorization": f"Bearer {self.local_token}"},
                json={"email": email, "password": password}
            )
            response.raise_for_status()
            result = response.json()
            self.factus_token = result["data"]["access_token"]
            self.refresh_token = result["data"]["refresh_token"]

    async def refresh_factus_token(self):
        """Refresh the Factus access token"""
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.base_url}/api/v1/auth/factus/refresh",
                headers={"Authorization": f"Bearer {self.local_token}"},
                json={"refresh_token": self.refresh_token}
            )
            response.raise_for_status()
            result = response.json()
            self.factus_token = result["data"]["access_token"]
            self.refresh_token = result["data"]["refresh_token"]

    def get_headers(self) -> dict:
        """Get headers for authenticated requests"""
        return {
            "Authorization": f"Bearer {self.local_token}",
            "X-Factus-Token": self.factus_token
        }

JavaScript/TypeScript

class FactusClient {
  private baseUrl: string;
  private localToken?: string;
  private factusToken?: string;
  private refreshToken?: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async loginLocal(username: string, password: string): Promise<void> {
    const response = await fetch(`${this.baseUrl}/api/v1/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({ username, password })
    });

    if (!response.ok) throw new Error('Local authentication failed');
    
    const data = await response.json();
    this.localToken = data.access_token;
  }

  async loginFactus(email: string, password: string): Promise<void> {
    const response = await fetch(`${this.baseUrl}/api/v1/auth/factus/login`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.localToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ email, password })
    });

    if (!response.ok) throw new Error('Factus authentication failed');
    
    const result = await response.json();
    this.factusToken = result.data.access_token;
    this.refreshToken = result.data.refresh_token;
  }

  async refreshFactusToken(): Promise<void> {
    const response = await fetch(`${this.baseUrl}/api/v1/auth/factus/refresh`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.localToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ refresh_token: this.refreshToken })
    });

    if (!response.ok) throw new Error('Token refresh failed');
    
    const result = await response.json();
    this.factusToken = result.data.access_token;
    this.refreshToken = result.data.refresh_token;
  }

  getHeaders(): Record<string, string> {
    return {
      'Authorization': `Bearer ${this.localToken}`,
      'X-Factus-Token': this.factusToken || ''
    };
  }
}

Error Handling

401 Unauthorized

Returned when the local JWT token is missing, invalid, or expired.
{
  "detail": "Could not validate credentials"
}
Solution: Re-authenticate with /api/v1/auth/login.

400 Bad Request (Factus Authentication)

Returned when Factus credentials are incorrect or the Factus service is unavailable.
{
  "detail": "No se pudo obtener el token de Factus: [error message]"
}
Solution: Verify your Factus credentials and service availability.

Security Best Practices

Never store tokens in:
  • Local storage (vulnerable to XSS)
  • URLs or query parameters
  • Client-side JavaScript files
  • Version control systems
Instead:
  • Use secure, httpOnly cookies for web applications
  • Store in environment variables for server-side applications
  • Use secure credential managers for mobile apps
Always use the refresh token mechanism to obtain new access tokens before expiration. Implement automatic token refresh in your client to avoid service interruptions.
Always use HTTPS in production to prevent token interception. Never send tokens over unencrypted HTTP connections.
Implement retry logic that attempts to refresh the token when receiving a 401 response, then retries the original request.

Next Steps

Creating Invoices

Learn how to create and submit invoices to Factus

Error Handling

Understand error responses and troubleshooting

Build docs developers (and LLMs) love