Developer reference

Gas Checker REST API

Versioned JSON API for the Flutter app. Base URL: https://your-domain.com/api/v1. All requests and responses use JSON except file uploads.

Envelope

Every response is { data, error, meta }.

Bearer auth

Authorization: Bearer <access_token>

Trial aware

402 responses include subscription state.

Response format

// Success
{ "data": { "id": "..." }, "error": null, "meta": null }

// List with pagination
{ "data": [ ... ], "error": null, "meta": { "count": 42, "limit": 50, "offset": 0 } }

// Failure
{ "data": null, "error": { "code": "SUBSCRIPTION_REQUIRED", "message": "...", "details": null } }
A 402 means the 30-day trial has ended. Show the paywall and call /subscription/checkout; do not treat it as a network failure.

Authentication

Signup and login return a Supabase access token and refresh token. Send the access token as a Bearer header on every protected request. Refresh when a request returns 401.

POST/api/v1/auth/signupCreate an account and start the 30-day Professional trial.

Create an account and start the 30-day Professional trial.

{
  "email": "engineer@example.com",
  "password": "min8chars",
  "full_name": "Kabbo Hasan",
  "company_name": "Hasan Heating",
  "phone": "07700 900000"
}
POST/api/v1/auth/loginSign in and receive tokens plus the account, plan and trial state.

Sign in and receive tokens plus the account, plan and trial state.

{ "email": "engineer@example.com", "password": "min8chars" }
POST/api/v1/auth/refreshExchange a refresh token for a new session.

Exchange a refresh token for a new session.

{ "refresh_token": "<refresh_token>" }
GET/api/v1/auth/meCurrent user, settings, subscription and trial days.

Current user, settings, subscription and trial days.

Requires Bearer token

POST/api/v1/auth/logoutRevoke the current token globally.

Revoke the current token globally.

Requires Bearer token

Plans & subscription

Read subscription state anywhere in the app. Creating business data requires an active plan; after the trial ends protected writes return 402 SUBSCRIPTION_REQUIRED.

GET/api/v1/plansPublic plan catalogue with monthly and yearly prices.

Public plan catalogue with monthly and yearly prices.

GET/api/v1/subscriptionCurrent subscription, plan and trial days left.

Current subscription, plan and trial days left.

Requires Bearer token

POST/api/v1/subscription/checkoutStart Stripe Checkout, or activate directly when Stripe is not configured.

Start Stripe Checkout, or activate directly when Stripe is not configured.

{ "plan_id": "pro", "billing_interval": "monthly" }

Requires Bearer token

POST/api/v1/subscription/cancelCancel at the end of the current period.

Cancel at the end of the current period.

Requires Bearer token

POST/api/v1/subscription/resumeUndo a pending cancellation.

Undo a pending cancellation.

Requires Bearer token

Contacts

Clients, installations and suppliers. Search across name, email, phone and postcode.

GET/api/v1/contactsList contacts.

List contacts.

Query: ?search=patel&type=client&limit=50&offset=0

Requires Bearer token

POST/api/v1/contactsCreate a contact.

Create a contact.

{ "name": "Mrs Patel", "phone": "07700 900000", "postcode": "M1 1AE", "type": "client" }

Requires Bearer token

GET/api/v1/contacts/:idFetch one contact.

Fetch one contact.

Requires Bearer token

PATCH/api/v1/contacts/:idUpdate supplied fields.

Update supplied fields.

Requires Bearer token

DELETE/api/v1/contacts/:idDelete a contact.

Delete a contact.

Requires Bearer token

Invoices & quotes

Invoices and quotes share the same document model and line items. Totals are calculated server-side from quantity × unit price plus tax.

GET/api/v1/invoicesList invoices with items and payments.

List invoices with items and payments.

Query: ?status=overdue&search=INV&limit=50&offset=0

Requires Bearer token

POST/api/v1/invoicesCreate an invoice. Set type to quote on /quotes.

Create an invoice. Set type to quote on /quotes.

{
  "client_name": "Mrs Patel",
  "issue_date": "2026-10-25",
  "due_date": "2026-11-08",
  "tax_rate": 20,
  "items": [
    { "description": "Boiler service", "quantity": 1, "unit_price": 85 }
  ]
}

Requires Bearer token

GET/api/v1/invoices/:idFetch an invoice with items and payments.

Fetch an invoice with items and payments.

Requires Bearer token

PATCH/api/v1/invoices/:idUpdate header fields or status.

Update header fields or status.

Requires Bearer token

DELETE/api/v1/invoices/:idDelete an invoice and its items.

Delete an invoice and its items.

Requires Bearer token

GET/api/v1/invoices/:id/paymentsList payments for an invoice.

List payments for an invoice.

Requires Bearer token

POST/api/v1/invoices/:id/paymentsRecord a payment. Invoice status and balance are recalculated automatically.

Record a payment. Invoice status and balance are recalculated automatically.

{ "amount": 85, "method": "bank", "reference": "BACS-123" }

Requires Bearer token

GET/api/v1/quotesList quotes.

List quotes.

Query: ?status=accepted&search=QT

Requires Bearer token

POST/api/v1/quotesCreate a quote using the same body as an invoice.

Create a quote using the same body as an invoice.

Requires Bearer token

GET/api/v1/quotes/:idFetch a quote with items.

Fetch a quote with items.

Requires Bearer token

PATCH/api/v1/quotes/:idUpdate a quote.

Update a quote.

Requires Bearer token

DELETE/api/v1/quotes/:idDelete a quote.

Delete a quote.

Requires Bearer token

POST/api/v1/quotes/:id/convertConvert a quote into a new draft invoice and mark the quote accepted.

Convert a quote into a new draft invoice and mark the quote accepted.

Requires Bearer token

Schedule & reminders

Calendar appointments and the follow-up reminders shown on the home screen.

GET/api/v1/appointmentsList appointments, optionally by date range and status.

List appointments, optionally by date range and status.

Query: ?from=2026-10-01T00:00:00Z&to=2026-11-01T00:00:00Z&status=scheduled

Requires Bearer token

POST/api/v1/appointmentsCreate an appointment.

Create an appointment.

{
  "title": "Landlord CP12",
  "location": "42 Elm Road",
  "starts_at": "2026-10-25T14:00:00Z",
  "alert_minutes": 60,
  "contact_id": null
}

Requires Bearer token

PATCH/api/v1/appointments/:idUpdate or mark an appointment complete.

Update or mark an appointment complete.

Requires Bearer token

DELETE/api/v1/appointments/:idDelete an appointment.

Delete an appointment.

Requires Bearer token

GET/api/v1/remindersList reminders.

List reminders.

Query: ?status=pending&kind=invoice

Requires Bearer token

POST/api/v1/remindersCreate a reminder for a gas report, invoice or custom task.

Create a reminder for a gas report, invoice or custom task.

{ "title": "Chase invoice", "kind": "invoice", "due_date": "2026-11-01" }

Requires Bearer token

PATCH/api/v1/reminders/:idMark done, dismissed or edit.

Mark done, dismissed or edit.

Requires Bearer token

DELETE/api/v1/reminders/:idDelete a reminder.

Delete a reminder.

Requires Bearer token

Gas reports

Gas safety certificates, warning notices and service records. Appliances, defects and readings are stored as JSON so the Flutter form can evolve without schema changes.

GET/api/v1/report-typesStatic list of supported certificate types and icons.

Static list of supported certificate types and icons.

Requires Bearer token

GET/api/v1/reportsList reports.

List reports.

Query: ?status=completed&type=domestic-landlord-gas-safety&search=CP12

Requires Bearer token

POST/api/v1/reportsCreate a report.

Create a report.

{
  "type": "domestic-landlord-gas-safety",
  "reference": "CP12-0042",
  "status": "draft",
  "property_postcode": "M1 1AE",
  "appliances": [{ "name": "Worcester 8000", "kw": 24 }],
  "readings": {}
}

Requires Bearer token

GET/api/v1/reports/:idFetch one report.

Fetch one report.

Requires Bearer token

PATCH/api/v1/reports/:idUpdate a report. Completed reports stamp completed_at.

Update a report. Completed reports stamp completed_at.

Requires Bearer token

DELETE/api/v1/reports/:idDelete a report.

Delete a report.

Requires Bearer token

Gas rate & dashboard

The calculator can return an unsaved preview or save the calculation to history.

POST/api/v1/gas-rateCalculate gross and net kW. Send save: false for a preview.

Calculate gross and net kW. Send save: false for a preview.

{ "meter_type": "metric", "reading1": 0, "reading2": 0.02, "seconds": 42, "save": true }

Requires Bearer token

GET/api/v1/gas-rateCalculation history, newest first.

Calculation history, newest first.

Query: ?limit=20&offset=0

Requires Bearer token

GET/api/v1/dashboard/summaryCounts, outstanding balance, upcoming jobs and reminders.

Counts, outstanding balance, upcoming jobs and reminders.

Requires Bearer token

Account, uploads & backup

Settings drive every generated document. Files are stored in the private Supabase bucket and returned as signed URLs.

GET/api/v1/settingsBusiness settings and document prefixes.

Business settings and document prefixes.

Requires Bearer token

PATCH/api/v1/settingsUpdate any supplied settings field.

Update any supplied settings field.

Requires Bearer token

PATCH/api/v1/profileUpdate name, company, phone, Gas Safe ID or avatar.

Update name, company, phone, Gas Safe ID or avatar.

Requires Bearer token

POST/api/v1/uploadsMultipart upload with fields file and folder (max 15 MB; JPEG, PNG, WebP, HEIC, PDF).

Multipart upload with fields file and folder (max 15 MB; JPEG, PNG, WebP, HEIC, PDF).

Requires Bearer token

GET/api/v1/uploads/signCreate a temporary signed URL.

Create a temporary signed URL.

Query: ?path=<storage_path>&expires=3600

Requires Bearer token

POST/api/v1/device-tokensRegister an FCM token for push notifications.

Register an FCM token for push notifications.

{ "token": "<fcm_token>", "platform": "android" }

Requires Bearer token

GET/api/v1/backupExport all data. Add raw=true to download a JSON file.

Export all data. Add raw=true to download a JSON file.

Requires Bearer token

POST/api/v1/backupStore a copy in Supabase Storage and record it in history.

Store a copy in Supabase Storage and record it in history.

Requires Bearer token

POST/api/v1/backup/restoreRestore a previous export. Merge keeps current data; replace deletes it first.

Restore a previous export. Merge keeps current data; replace deletes it first.

{ "backup": { "version": 1, "data": { "...": "..." } }, "mode": "merge" }

Requires Bearer token

Errors

StatusCodeMeaning
400INVALID_JSONThe request body is not valid JSON.
401UNAUTHORIZED / INVALID_TOKENMissing, expired or invalid access token.
402SUBSCRIPTION_REQUIREDTrial ended and no active plan. Send the user to billing.
403FORBIDDENThe record belongs to another user.
404NOT_FOUNDRecord does not exist for this user.
413FILE_TOO_LARGEUpload exceeds 15 MB.
415UNSUPPORTED_FILEOnly images or PDF files are allowed.
422VALIDATION_ERRORField validation failed; details contains the field errors.
503DATABASE_NOT_READYSupabase migration or exposed schema has not been applied.

Flutter client

Store both tokens securely, attach the access token to each call, and refresh once on a 401 before signing the user out.

import 'dart:convert';
import 'package:http/http.dart' as http;

class ApiClient {
  ApiClient({required this.baseUrl, required this.getToken, required this.onRefresh});

  final String baseUrl;              // https://your-domain.com/api/v1
  final Future<String?> Function() getToken;
  final Future<bool> Function() onRefresh;

  Future<Map<String, dynamic>> request(
    String method,
    String path, {
    Map<String, dynamic>? body,
    bool retried = false,
  }) async {
    final token = await getToken();
    final response = await http.Request(method, Uri.parse('$baseUrl$path'))
      ..headers.addAll({
        'content-type': 'application/json',
        if (token != null) 'authorization': 'Bearer $token',
      })
      ..body = body == null ? '' : jsonEncode(body);

    final streamed = await response.send();
    final decoded = jsonDecode(await streamed.stream.bytesToString());

    if (streamed.statusCode == 401 && !retried && await onRefresh()) {
      return request(method, path, body: body, retried: true);
    }
    if (streamed.statusCode >= 400) {
      throw ApiException(
        code: decoded['error']?['code'] ?? 'UNKNOWN',
        message: decoded['error']?['message'] ?? 'Request failed',
        status: streamed.statusCode,
      );
    }
    return decoded['data'] as Map<String, dynamic>;
  }
}

For multipart uploads use MultipartRequest to /api/v1/uploads with fields file and folder.

Ready to power your Flutter app?

Create an account to receive a trial token pair.

Get an API account