Files
accounted/dev_docs/04-API-SPECIFICATION.md
T
2026-02-13 14:00:46 +01:00

13 KiB
Raw Blame History

API Specification

Authentication

All API routes require authentication via Supabase session cookie.

// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = cookies()
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
      },
    }
  )
}

// Usage in route
export async function GET() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  
  if (!user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }
  
  // ... route logic
}

Endpoints

Company Settings

GET /api/settings

Returns current user's company settings.

Response 200:

{
  "id": "uuid",
  "company_name": "Anna Andersson Content AB",
  "org_number": "123456-7890",
  "vat_number": "SE1234567890",
  "vat_registered": true,
  "address_line1": "Storgatan 1",
  "city": "Stockholm",
  "postal_code": "11122",
  "payment_terms_days": 30,
  "next_invoice_number": 15
}

PUT /api/settings

Updates company settings.

Request:

{
  "company_name": "Anna Andersson Content AB",
  "municipal_tax_rate": 31.5,
  "payment_terms_days": 14
}

Transactions

GET /api/transactions

List transactions with optional filters.

Query params:

  • category: filter by category
  • from: start date (YYYY-MM-DD)
  • to: end date
  • uncategorized: boolean, only uncategorized
  • limit: default 50
  • offset: pagination

Response 200:

{
  "data": [
    {
      "id": "uuid",
      "booking_date": "2024-01-15",
      "amount": -1500.00,
      "currency": "SEK",
      "description": "ADOBE SYSTEMS",
      "counterparty_name": "Adobe",
      "category": "uncategorized",
      "expense_type": null
    }
  ],
  "total": 142,
  "uncategorized_count": 23
}

PATCH /api/transactions/:id

Categorize a transaction.

Request:

{
  "category": "business_expense",
  "expense_type": "software",
  "business_percentage": 100,
  "notes": "Creative Cloud subscription"
}

POST /api/transactions/bulk-categorize

Categorize multiple transactions at once.

Request:

{
  "transaction_ids": ["uuid1", "uuid2"],
  "category": "private"
}

Banking

POST /api/banking/connect

Initiates PSD2 bank connection flow.

Request:

{
  "provider": "tink"
}

Response 200:

{
  "redirect_url": "https://link.tink.com/..."
}

GET /api/banking/callback

OAuth callback from banking provider. Handles token exchange.

Query params: Provider-specific (code, state, etc.)

Redirects to: /transactions?connected=true

POST /api/banking/sync

Manually trigger transaction sync. Also called by cron.

Response 200:

{
  "synced": 12,
  "new_transactions": 3
}

Customers

GET /api/customers

List all customers.

Response 200:

{
  "data": [
    {
      "id": "uuid",
      "name": "Influencer Agency AB",
      "customer_type": "swedish_business",
      "org_number": "556677-8899",
      "country": "SE",
      "invoice_count": 5
    }
  ]
}

POST /api/customers

Create new customer.

Request:

{
  "name": "Google Ireland Ltd",
  "customer_type": "eu_business",
  "vat_number": "IE6388047V",
  "address_line1": "Gordon House",
  "city": "Dublin",
  "country": "IE",
  "email": "payments@google.com"
}

Response 201: Created customer object

GET /api/customers/:id

Get single customer with invoice history.

PUT /api/customers/:id

Update customer.

DELETE /api/customers/:id

Soft delete (only if no invoices linked).


VAT Validation

POST /api/vat/validate

Validate EU VAT number via VIES (VAT Information Exchange System).

Critical for Reverse Charge: Swedish law requires validated VAT number before applying 0% rate on EU B2B sales. Without validation, you must charge 25% Swedish VAT.

Request:

{
  "vat_number": "IE6388047V"
}

Response 200:

{
  "valid": true,
  "country_code": "IE",
  "vat_number": "6388047V",
  "name": "GOOGLE IRELAND LIMITED",
  "address": "GORDON HOUSE, BARROW STREET, DUBLIN 4",
  "validated_at": "2024-01-15T10:30:00Z"
}

Response 200 (Invalid):

{
  "valid": false,
  "country_code": "IE",
  "vat_number": "INVALID123",
  "error": "VAT number not found in VIES database"
}

Implementation Notes:

  • Use EU VIES SOAP service or REST wrapper
  • Cache validation for 24 hours (VAT numbers rarely change)
  • Store validated_at timestamp on customer record
  • Re-validate periodically for long-term customers

Foreign Purchase Handling (Fiktiv Moms)

POST /api/transactions/:id/foreign-purchase

Mark a transaction as foreign service purchase requiring fiktiv moms.

When influencers buy services from abroad (Adobe CC, Facebook Ads, AWS), they must self-report VAT.

Request:

{
  "supplier_country": "US",
  "service_type": "digital_service",
  "vat_rate": 25
}

Response 200:

{
  "transaction_id": "uuid",
  "fiktiv_moms_amount": 375.00,
  "bookings": [
    { "account": "2645", "debit": 375.00, "description": "Beräknad ingående moms utländskt förvärv" },
    { "account": "2614", "credit": 375.00, "description": "Utgående moms utländskt förvärv" }
  ],
  "moms_ruta_21": 1500.00,
  "moms_ruta_48": 375.00
}

Momsdeklaration Impact:

  • Ruta 21: Purchase amount (before VAT)
  • Ruta 48: Self-reported output VAT
  • Net effect is zero if input VAT is deductible, but both must be declared

---

### Invoices

#### GET /api/invoices

List invoices with filters.

**Query params:**
- `status`: draft, sent, paid, overdue
- `customer_id`: filter by customer
- `from`, `to`: date range

**Response 200:**
```json
{
  "data": [
    {
      "id": "uuid",
      "invoice_number": "INV-2024-015",
      "customer": {
        "id": "uuid",
        "name": "Agency AB"
      },
      "invoice_date": "2024-01-15",
      "due_date": "2024-02-14",
      "total": 25000.00,
      "currency": "SEK",
      "status": "sent"
    }
  ]
}

POST /api/invoices

Create new invoice.

Request:

{
  "customer_id": "uuid",
  "invoice_date": "2024-01-15",
  "due_date": "2024-02-14",
  "reference": "PO-12345",
  "items": [
    {
      "description": "Instagram kampanj November",
      "quantity": 1,
      "unit": "st",
      "unit_price": 20000.00
    }
  ],
  "customer_notes": "Tack för samarbetet!"
}

The API automatically determines VAT treatment based on customer type:

// lib/invoice/vat-rules.ts

interface VatDecision {
  rate: number
  reverseCharge: boolean
  invoiceText: string | null
  momsRuta: number  // Which ruta in momsdeklaration
}

function determineVatTreatment(customer: Customer, settings: CompanySettings): VatDecision {
  // User not VAT registered - no VAT on any invoice
  if (!settings.vat_registered) {
    return { 
      rate: 0, 
      reverseCharge: false, 
      invoiceText: 'Säljaren är inte momsregistrerad',
      momsRuta: 0  // Not applicable
    }
  }
  
  switch (customer.customer_type) {
    case 'swedish_business':
    case 'individual':
      return { 
        rate: 25, 
        reverseCharge: false, 
        invoiceText: null,
        momsRuta: 5  // Ruta 05: Momspliktig försäljning
      }
    
    case 'eu_business':
      if (customer.vat_number_validated) {
        return {
          rate: 0,
          reverseCharge: true,
          invoiceText: 'Omvänd skattskyldighet / Reverse charge - Article 196 Council Directive 2006/112/EC',
          momsRuta: 39  // Ruta 39: Tjänsteförsäljning EU
        }
      }
      // EU business without valid VAT = charge Swedish VAT
      return { 
        rate: 25, 
        reverseCharge: false, 
        invoiceText: null,
        momsRuta: 5
      }
    
    case 'non_eu_business':
      return {
        rate: 0,
        reverseCharge: false,
        invoiceText: 'Export av tjänst - moms utgår ej',
        momsRuta: 40  // Ruta 40: Export
      }
  }
}

Momsdeklaration Ruta Reference:

Ruta Description When Used
05 Momspliktig försäljning Swedish domestic sales
39 Tjänsteförsäljning EU EU B2B with reverse charge
40 Export Non-EU sales
21 Inköp av tjänster från EU Foreign service purchases (input)
48 Utgående moms på inköp Fiktiv moms on foreign purchases

EU Sales Reporting: Transactions with momsRuta: 39 must be aggregated into Periodisk Sammanställning (quarterly report to Skatteverket), separate from regular momsdeklaration.

Response 201: Created invoice with calculated totals

GET /api/invoices/:id

Get full invoice details including items.

PUT /api/invoices/:id

Update draft invoice. Cannot modify sent/paid invoices.

POST /api/invoices/:id/send

Mark invoice as sent and optionally email to customer.

Request:

{
  "send_email": true,
  "email_to": "invoice@client.com",
  "email_message": "Hej! Här kommer fakturan för vårt samarbete."
}

Email Implementation: Uses Supabase built-in email via Edge Functions.

Response 200:

{
  "id": "uuid",
  "status": "sent",
  "sent_at": "2024-01-15T10:30:00Z",
  "email_sent": true
}

Note: Invoice PDF attached to email. Payment method shown: bank transfer only (Bankgiro/IBAN). Future: third-party payment links (Stripe, Klarna).

POST /api/invoices/:id/mark-paid

Mark invoice as paid.

Request:

{
  "paid_at": "2024-02-10",
  "paid_amount": 25000.00,
  "linked_transaction_id": "uuid"
}

GET /api/invoices/:id/pdf

Generate and return invoice PDF.

Query params:

  • regenerate: force regenerate cached PDF

Response: PDF file or redirect to signed storage URL


Salary Payments (Aktiebolag only)

GET /api/salary

List salary payments for current fiscal year.

Response 200:

{
  "data": [
    {
      "id": "uuid",
      "payment_date": "2024-01-25",
      "pay_period_start": "2024-01-01",
      "pay_period_end": "2024-01-31",
      "gross_salary": 50000.00,
      "employer_contributions": 15710.00,
      "withheld_tax": 15000.00,
      "net_salary": 35000.00,
      "total_cost": 65710.00,
      "agi_reported": false
    }
  ],
  "ytd_totals": {
    "gross_salary": 50000.00,
    "employer_contributions": 15710.00,
    "withheld_tax": 15000.00,
    "total_cost": 65710.00
  }
}

POST /api/salary

Create salary payment record.

Request:

{
  "pay_period_start": "2024-01-01",
  "pay_period_end": "2024-01-31",
  "gross_salary": 50000.00,
  "tax_table": 33
}

Automatic calculations:

  • employer_contributions: gross × 31.42%
  • withheld_tax: from Swedish tax tables based on gross + table number
  • net_salary: gross - withheld_tax
  • total_cost: gross + employer_contributions

Response 201: Created salary payment with all calculated fields.

POST /api/salary/:id/mark-agi-reported

Mark salary payment as reported in AGI-deklaration.

Response 200:

{
  "id": "uuid",
  "agi_reported": true,
  "agi_reported_at": "2024-02-12T14:00:00Z"
}

Dashboard

GET /api/dashboard/summary

Get aggregated financial overview.

Response 200:

{
  "period": {
    "year": 2024,
    "month": 1
  },
  "revenue": {
    "ytd": 450000.00,
    "this_month": 75000.00
  },
  "expenses": {
    "ytd": 85000.00,
    "this_month": 12000.00
  },
  "net_income": {
    "ytd": 365000.00
  },
  "tax_estimate": {
    "egenavgifter": 105745.00,
    "income_tax": 116800.00,
    "vat_payable": 91250.00,
    "total_locked": 313795.00
  },
  "disponibelt": 136205.00,
  "vat_threshold": {
    "limit": 80000,
    "current": 450000.00,
    "registered": true
  },
  "pending": {
    "uncategorized_transactions": 23,
    "unpaid_invoices": 2,
    "unpaid_amount": 45000.00
  }
}

GET /api/dashboard/chart

Monthly revenue/expense data for charts.

Query params:

  • months: number of months (default 12)

Response 200:

{
  "data": [
    {
      "month": "2024-01",
      "revenue": 75000,
      "expenses": 12000,
      "net": 63000
    }
  ]
}

Error Responses

All endpoints return errors in consistent format:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid VAT number format",
    "details": {
      "field": "vat_number",
      "value": "invalid"
    }
  }
}

Common status codes:

  • 400: Validation error
  • 401: Not authenticated
  • 403: Forbidden (RLS violation)
  • 404: Resource not found
  • 409: Conflict (duplicate invoice number)
  • 500: Internal error