RESTful API endpoints, request/response formats, and organization context handling.
------
All API endpoints follow REST conventions and return JSON responses.
**Base URL:**/api**Authentication:** Laravel Sanctum (Bearer token or session-based)
**Headers:**http
Content-Type: application/json
Accept: application/json
Authorization: Bearer {token}
X-Organization-Id: {org_id} (optional, for API calls)
---
Organization Context
For API calls, organization context can be specified via header:
http
GET /api/time-blocks
X-Organization-Id: 123
Authorization: Bearer {token}
Or via session for web routes (automatically set from user's current organization).---
Authentication Endpoints
POST /register # Register + create org
POST /login # Login
POST /logout # Logout
POST /forgot-password # Request reset
POST /reset-password # Reset password
POST /register
Create new user and organization.
**Request:**
json
{
"name": "John Doe",
"email": "john@example.com",
"password": "secret123",
"password_confirmation": "secret123",
"organization_name": "Acme Agency"
}
**Response (201):**
json
{
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"organization": {
"id": 1,
"name": "Acme Agency",
"slug": "acme-agency"
},
"token": "1|abc123..."
}
POST /login
**Request:**
json
{
"email": "john@example.com",
"password": "secret123"
}
**Response (200):**
json
{
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"token": "2|xyz789..."
}
---
Organization Endpoints
GET /api/organizations # List user's organizations
POST /api/organizations # Create new organization
GET /api/organizations/{org} # Get organization details
PUT /api/organizations/{org} # Update organization
DELETE /api/organizations/{org} # Delete organization (owner only)
POST /api/organizations/{org}/switch # Switch to this organization
GET /api/organizations
List all organizations the user belongs to.
**Response (200):**
json
{
"data": [
{
"id": 1,
"name": "Acme Agency",
"slug": "acme-agency",
"role": "owner",
"is_current": true
},
{
"id": 2,
"name": "Freelance",
"slug": "freelance",
"role": "member",
"is_current": false
}
]
}
POST /api/organizations/{org}/switch
Switch user's current organization context.
**Response (200):**
json
{
"message": "Switched to Freelance",
"organization": {
"id": 2,
"name": "Freelance",
"slug": "freelance"
}
}
---
Team Management Endpoints
GET /api/team # List org members
POST /api/team/invite # Invite user
PUT /api/team/{user} # Update user role
DELETE /api/team/{user} # Remove user from org
GET /api/invitations/{token} # Get invitation details
POST /api/invitations/{token}/accept # Accept invitation
GET /api/team
**Response (200):**
json
{
"data": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "owner",
"joined_at": "2024-01-01T00:00:00Z"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com",
"role": "member",
"joined_at": "2024-01-15T00:00:00Z"
}
],
"pending_invitations": [
{
"email": "new@example.com",
"role": "member",
"invited_at": "2024-01-20T00:00:00Z",
"expires_at": "2024-01-27T00:00:00Z"
}
]
}
POST /api/team/invite
**Request:**
json
{
"email": "new@example.com",
"role": "member"
}
**Response (201):**
json
{
"message": "Invitation sent to new@example.com",
"invitation": {
"email": "new@example.com",
"role": "member",
"expires_at": "2024-01-27T00:00:00Z"
}
}
PUT /api/team/{user}
**Request:**
json
{
"role": "admin"
}
**Response (200):**
json
{
"message": "Role updated",
"user": {
"id": 2,
"name": "Jane Smith",
"role": "admin"
}
}
---
Integration Endpoints
GET /api/integrations # List org integrations
GET /api/integrations/available # List available providers
POST /integrations/{provider}/connect # Start OAuth flow
GET /integrations/{provider}/callback # OAuth callback
DELETE /api/integrations/{id} # Disconnect integration
GET /api/integrations/{id}/calendars # List calendars
PUT /api/calendars/{id} # Update calendar settings
POST /api/calendars/{id}/sync # Force sync
GET /api/integrations
**Response (200):**
json
{
"data": [
{
"id": 1,
"provider": "google_calendar",
"provider_type": "calendar",
"name": "John's Google Calendar",
"account_email": "john@gmail.com",
"is_active": true,
"last_sync_at": "2024-01-20T10:00:00Z",
"calendars": [
{
"id": 1,
"name": "Primary",
"color": "#4285f4",
"is_enabled": true
}
]
},
{
"id": 2,
"provider": "moneybird",
"provider_type": "invoicing",
"name": "My Company BV",
"is_active": true,
"last_sync_at": "2024-01-20T09:30:00Z"
}
]
}
GET /api/integrations/available
**Response (200):**
json
{
"data": {
"calendar": [
{
"key": "google_calendar",
"name": "Google Calendar",
"description": "Connect your Google Calendar",
"icon": "google"
},
{
"key": "office365_calendar",
"name": "Office 365",
"description": "Connect your Outlook calendar",
"icon": "microsoft",
"available": false,
"coming_soon": true
}
],
"invoicing": [
{
"key": "moneybird",
"name": "Moneybird",
"description": "Track time and create invoices",
"icon": "moneybird"
}
]
}
}
PUT /api/calendars/{id}
**Request:**
json
{
"is_enabled": true,
"sync_direction": "both"
}
**Response (200):**
json
{
"message": "Calendar settings updated",
"calendar": {
"id": 1,
"name": "Primary",
"is_enabled": true,
"sync_direction": "both"
}
}
---
Time Block Endpoints
GET /api/time-blocks # List (with date range filter)
POST /api/time-blocks # Create
GET /api/time-blocks/{id} # Get single
PUT /api/time-blocks/{id} # Update
DELETE /api/time-blocks/{id} # Delete
POST /api/time-blocks/{id}/sync # Force sync to integrations
GET /api/time-blocks
**Query Parameters:**
• start (required): ISO 8601 date (e.g., 2024-01-08)
• end (required): ISO 8601 date (e.g., 2024-01-14)
• user_id (optional): Filter by user
• client_id (optional): Filter by client
• project_id (optional): Filter by project
• calendar_id (optional): Filter by calendar
**Response (200):**
json
{
"data": [
{
"id": 1,
"title": "Client Meeting",
"description": "Discuss project requirements",
"start_time": "2024-01-08T09:00:00+01:00",
"end_time": "2024-01-08T10:00:00+01:00",
"timezone": "Europe/Amsterdam",
"color": "#0D9488",
"is_billable": true,
"client": {
"id": 1,
"name": "Acme Corp"
},
"project": {
"id": 1,
"name": "Website Redesign"
},
"user": {
"id": 1,
"name": "John Doe"
},
"calendar_sync_status": "synced",
"invoicing_sync_status": "synced",
"external_ids": {
"calendar_event_id": "abc123",
"invoicing_entry_id": "456789"
}
}
],
"meta": {
"total_hours": 32.5,
"billable_hours": 28.0,
"by_day": {
"2024-01-08": 8.0,
"2024-01-09": 7.5,
"2024-01-10": 8.0,
"2024-01-11": 6.0,
"2024-01-12": 3.0
}
}
}
POST /api/time-blocks
**Request:**
json
{
"title": "Development Work",
"description": "Implement new feature",
"start_time": "2024-01-08T09:00:00",
"end_time": "2024-01-08T12:00:00",
"client_id": 1,
"project_id": 1,
"calendar_id": 1,
"is_billable": true,
"color": "#0D9488"
}
**Response (201):**
json
{
"data": {
"id": 2,
"title": "Development Work",
"start_time": "2024-01-08T09:00:00+01:00",
"end_time": "2024-01-08T12:00:00+01:00",
"calendar_sync_status": "pending",
"invoicing_sync_status": "pending"
},
"message": "Time block created. Syncing to integrations..."
}
PUT /api/time-blocks/{id}
**Request:**
json
{
"title": "Development Work (Updated)",
"start_time": "2024-01-08T10:00:00",
"end_time": "2024-01-08T13:00:00"
}
**Response (200):**
json
{
"data": {
"id": 2,
"title": "Development Work (Updated)",
"start_time": "2024-01-08T10:00:00+01:00",
"end_time": "2024-01-08T13:00:00+01:00",
"calendar_sync_status": "pending",
"invoicing_sync_status": "pending"
},
"message": "Time block updated. Syncing to integrations..."
}
POST /api/time-blocks/{id}/sync
Force re-sync a time block to external services.
**Response (200):**
json
{
"message": "Sync initiated",
"sync_status": {
"calendar": "syncing",
"invoicing": "syncing"
}
}
---
Client & Project Endpoints
GET /api/clients # List
POST /api/clients # Create
PUT /api/clients/{id} # Update
DELETE /api/clients/{id} # Delete
GET /api/projects # List
POST /api/projects # Create
PUT /api/projects/{id} # Update
DELETE /api/projects/{id} # Delete
GET /api/clients
**Query Parameters:**
• active (optional): Filter by active status (true/false)
• search (optional): Search by name
**Response (200):**
json
{
"data": [
{
"id": 1,
"name": "Acme Corp",
"email": "contact@acme.com",
"external_id": "789012",
"is_active": true,
"projects_count": 3
}
]
}
GET /api/projects
**Query Parameters:**
• client_id (optional): Filter by client
• active (optional): Filter by active status
**Response (200):**
json
{
"data": [
{
"id": 1,
"name": "Website Redesign",
"color": "#4285f4",
"hourly_rate": 125.00,
"external_id": "123456",
"is_active": true,
"client": {
"id": 1,
"name": "Acme Corp"
}
}
]
}
---
Settings Endpoints
GET /api/settings # Get org + user settings
PUT /api/settings # Update settings
GET /api/preferences # Get user preferences
PUT /api/preferences # Update preferences
GET /api/preferences
**Response (200):**
json
{
"data": {
"timezone": "Europe/Amsterdam",
"work_days": ["mon", "tue", "wed", "thu", "fri"],
"work_hours_start": "08:00",
"work_hours_end": "18:00",
"preferences": {
"theme": "light",
"notifications": true
}
}
}
PUT /api/preferences
**Request:**
json
{
"timezone": "Europe/London",
"work_days": ["mon", "tue", "wed", "thu"],
"work_hours_start": "09:00",
"work_hours_end": "17:00"
}
**Response (200):**
json
{
"message": "Preferences updated",
"data": {
"timezone": "Europe/London",
"work_days": ["mon", "tue", "wed", "thu"],
"work_hours_start": "09:00",
"work_hours_end": "17:00"
}
}
GET /api/settings
Organization settings (admin/owner only).
**Response (200):**
json
{
"data": {
"organization": {
"id": 1,
"name": "Acme Agency",
"slug": "acme-agency",
"timezone": "Europe/Amsterdam",
"billing_email": "billing@acme.com",
"subscription_status": "active"
},
"integrations_count": {
"calendar": 1,
"invoicing": 1
},
"team_count": 5
}
}
---
Error Responses
All errors follow a consistent format:
**401 Unauthorized:**
json
{
"message": "Unauthenticated."
}
**403 Forbidden:**
json
{
"message": "You do not have permission to perform this action."
}
**404 Not Found:**
json
{
"message": "Time block not found."
}
**422 Validation Error:**
json
{
"message": "The given data was invalid.",
"errors": {
"title": ["The title field is required."],
"start_time": ["The start time must be before end time."]
}
}
**500 Server Error:**
json
{
"message": "An error occurred. Please try again."
}
``