danwel - API Design

RESTful API endpoints, request/response formats, and organization context handling.

---

Table of Contents



1. [API Overview](#api-overview)
2. [Organization Context](#organization-context)
3. [Authentication Endpoints](#authentication-endpoints)
4. [Organization Endpoints](#organization-endpoints)
5. [Team Management Endpoints](#team-management-endpoints)
6. [Integration Endpoints](#integration-endpoints)
7. [Time Block Endpoints](#time-block-endpoints)
8. [Client & Project Endpoints](#client--project-endpoints)
9. [Settings Endpoints](#settings-endpoints)

---

API Overview

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."
    }
    ``