MENU navbar-image

Introduction

API REST di SunPilot: anagrafica impianti fotovoltaici e telemetria canonica, vendor-agnostic.

L'API v1 di SunPilot espone anagrafica impianti fotovoltaici e telemetria canonica del tuo tenant, qualunque sia il vendor dell'hardware in campo.

Base URL: https://sunpilot.cloud/api/v1 · Versioning: il prefisso /v1 è il contratto; le evoluzioni incompatibili arriveranno come /v2.

Convenzioni:

Authenticating requests

To authenticate requests, include an Authorization header with the value "Bearer {ACCESS_TOKEN}".

All authenticated endpoints are marked with a requires authentication badge in the documentation below.

Le credenziali OAuth2 (client id/secret) si ottengono su richiesta scrivendo a info@sunpilot.cloud. Ottenute le credenziali, il flusso è authorization code + PKCE: autorizzazione su /oauth/authorize, scambio del codice su /oauth/token, refresh su /oauth/token/refresh. Scope disponibili: read (lettura di anagrafica e telemetria) e write (scrittura di anagrafica; le route di scrittura richiedono anche un ruolo abilitato alla scrittura nel tenant). Lo scope command è riservato per i futuri comandi verso i device e oggi non è consumato da nessun endpoint.

Autenticazione

Issue an access token.

requires authentication

Example request:
curl --request POST \
    "https://sunpilot.cloud/oauth/token" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/oauth/token';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/oauth/token"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Request      

POST oauth/token

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Authorize a client to access the user's account.

requires authentication

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/oauth/authorize" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/oauth/authorize';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/oauth/authorize"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET oauth/authorize

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Get a fresh transient token cookie for the authenticated user.

requires authentication

Example request:
curl --request POST \
    "https://sunpilot.cloud/oauth/token/refresh" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/oauth/token/refresh';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/oauth/token/refresh"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "POST",
    headers,
}).then(response => response.json());

Request      

POST oauth/token/refresh

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Identità

Chi sono (senza tenant)

requires authentication

A differenza di GET /me, non richiede un tenant risolto: è l'endpoint da chiamare subito dopo l'auth, prima ancora di scegliere il tenant.

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/whoami" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/whoami';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/whoami"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET api/v1/whoami

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Chi sono

requires authentication

Restituisce l'utente autenticato, il tenant in cui opera il token e il suo ruolo. Utile come primo endpoint di verifica dopo aver ottenuto un access token.

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/me" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/me';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/me"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "user": {
        "id": 1,
        "name": "Mario Rossi",
        "email": "mario@installatore.it"
    },
    "tenant": {
        "id": 1
    },
    "role": "owner"
}
 

Request      

GET api/v1/me

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Impianti

Gli impianti (sites) sono l'anagrafica di base: ogni device, misura e KPI appartiene a un impianto del tuo tenant.

Elenca gli impianti

requires authentication

Impianti del tenant, paginati, con stato di collegamento cloud e ultima misura.

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/sites?per_page=25" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/sites';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'per_page' => '25',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/sites"
);

const params = {
    "per_page": "25",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": 1,
            "name": "Capannone Rossi",
            "timezone": "Europe/Rome",
            "cloud_connected": true,
            "cloud_links": [
                {
                    "id": "0198f2b0-1a2b-73e0-8b1c-8f6c2e2b9a10",
                    "provider": "fusionsolar",
                    "label": null,
                    "station_code": "NE=12345678",
                    "last_synced_at": "2026-07-09T11:45:00.000000Z",
                    "backfill_status": null,
                    "backfill_result": null,
                    "backfill_error": null
                }
            ],
            "online": true,
            "last_seen_at": "2026-07-09T11:45:12.000000Z",
            "last_measurement_at": "2026-07-09T11:00:00.000000Z",
            "latitude": 45.4642,
            "longitude": 9.19,
            "address": "Via Baldini 12, Milano",
            "peak_power_kwp": 60,
            "tilt": 10,
            "azimuth": 0,
            "has_pv_geometry": true,
            "diagnostics_severity": null,
            "diagnostics_computed_at": null
        }
    ],
    "links": {
        "first": "https://sunpilot.cloud/api/v1/sites?page=1",
        "last": "https://sunpilot.cloud/api/v1/sites?page=1",
        "prev": null,
        "next": null
    },
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 1
    }
}
 

Request      

GET api/v1/sites

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Risultati per pagina (1–1000). Default: 15. Example: 25

Dettaglio impianto

requires authentication

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/sites/1" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/sites/1';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/sites/1"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": {
        "id": 1,
        "name": "Capannone Rossi",
        "timezone": "Europe/Rome",
        "cloud_connected": true,
        "cloud_links": [
            {
                "id": "0198f2b0-1a2b-73e0-8b1c-8f6c2e2b9a10",
                "provider": "fusionsolar",
                "label": null,
                "station_code": "NE=12345678",
                "last_synced_at": "2026-07-09T11:45:00.000000Z",
                "backfill_status": null,
                "backfill_result": null,
                "backfill_error": null
            }
        ],
        "online": true,
        "last_seen_at": "2026-07-09T11:45:12.000000Z",
        "last_measurement_at": "2026-07-09T11:00:00.000000Z",
        "latitude": 45.4642,
        "longitude": 9.19,
        "address": "Via Baldini 12, Milano",
        "peak_power_kwp": 60,
        "tilt": 10,
        "azimuth": 0,
        "has_pv_geometry": true,
        "diagnostics_severity": null,
        "diagnostics_computed_at": null
    }
}
 

Example response (404):


{
    "message": "Not found."
}
 

Request      

GET api/v1/sites/{site_id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

site_id   integer     

ID dell'impianto. Example: 1

Dettaglio impianto (aggregato)

requires authentication

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/sites/1/detail" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/sites/1/detail';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/sites/1/detail"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET api/v1/sites/{site_id}/detail

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

site_id   integer     

ID dell'impianto. Example: 1

Elenco dispositivi del sito

requires authentication

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/sites/architecto/devices" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/sites/architecto/devices';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/sites/architecto/devices"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET api/v1/sites/{site_id}/devices

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

site_id   string     

The ID of the site. Example: architecto

Dettaglio dispositivo

requires authentication

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/devices/architecto" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/devices/architecto';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/devices/architecto"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET api/v1/devices/{device_id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

device_id   string     

The ID of the device. Example: architecto

Crea un impianto

requires authentication

Richiede scope write e un ruolo abilitato alla scrittura. Se il piano del tenant ha raggiunto il limite di impianti risponde 402 con error: plan_limit.

Example request:
curl --request POST \
    "https://sunpilot.cloud/api/v1/sites" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"name\": \"Capannone Rossi\",
    \"customer_label\": \"n\",
    \"timezone\": \"Europe\\/Rome\",
    \"address\": \"Via Roma 1, Milano\",
    \"latitude\": -90,
    \"longitude\": -180,
    \"peak_power_kwp\": 6.5,
    \"battery_capacity_kwh\": 1,
    \"commissioned_date\": \"2024-05-10\"
}"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/sites';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'name' => 'Capannone Rossi',
            'customer_label' => 'n',
            'timezone' => 'Europe/Rome',
            'address' => 'Via Roma 1, Milano',
            'latitude' => -90,
            'longitude' => -180,
            'peak_power_kwp' => 6.5,
            'battery_capacity_kwh' => 1,
            'commissioned_date' => '2024-05-10',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/sites"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "name": "Capannone Rossi",
    "customer_label": "n",
    "timezone": "Europe\/Rome",
    "address": "Via Roma 1, Milano",
    "latitude": -90,
    "longitude": -180,
    "peak_power_kwp": 6.5,
    "battery_capacity_kwh": 1,
    "commissioned_date": "2024-05-10"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (201):


{
    "data": {
        "id": 2,
        "name": "Capannone Rossi",
        "timezone": "Europe/Rome",
        "cloud_connected": false,
        "cloud_links": [],
        "online": false,
        "last_seen_at": null,
        "last_measurement_at": null,
        "latitude": null,
        "longitude": null,
        "address": null,
        "peak_power_kwp": null,
        "tilt": null,
        "azimuth": null,
        "has_pv_geometry": false
    }
}
 

Example response (402):


{
    "error": "plan_limit",
    "message": "Hai raggiunto il limite di impianti del piano Free. Passa a un piano superiore per aggiungerne altri."
}
 

Request      

POST api/v1/sites

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Nome dell'impianto. Example: Capannone Rossi

customer_label   string  optional    

Must not be greater than 255 characters. Example: n

timezone   string  optional    

Timezone IANA. Default: Europe/Rome se omesso — il client dovrebbe passare il fuso rilevato lato browser. Example: Europe/Rome

address   string  optional    

Indirizzo dell'impianto. Example: Via Roma 1, Milano

latitude   number  optional    

Must be between -90 and 90. Example: -90

longitude   number  optional    

Must be between -180 and 180. Example: -180

peak_power_kwp   number  optional    

Potenza di picco installata (kWp). Example: 6.5

battery_capacity_kwh   number  optional    

Must be at least 0. Must not be greater than 999999.99. Example: 1

commissioned_date   string  optional    

Data di messa in servizio (formato data). Example: 2024-05-10

Telemetria

KPI del sito

requires authentication

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/sites/architecto/kpis" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/sites/architecto/kpis';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/sites/architecto/kpis"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET api/v1/sites/{site_id}/kpis

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

site_id   string     

The ID of the site. Example: architecto

Serie energetica del sito

requires authentication

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/sites/architecto/energy-series" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/sites/architecto/energy-series';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/sites/architecto/energy-series"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET api/v1/sites/{site_id}/energy-series

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

site_id   string     

The ID of the site. Example: architecto

Serie produzione del sito

requires authentication

Serie produzione filtrabile (giorno/settimana/mese) atteso-vs-reale. Finestra di default per granularità; from/to opzionali (Y-m-d).

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/sites/architecto/production-series" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/sites/architecto/production-series';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/sites/architecto/production-series"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET api/v1/sites/{site_id}/production-series

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

site_id   string     

The ID of the site. Example: architecto

Misure di un device

requires authentication

Serie temporale delle misure canoniche di un device, dalla più recente. Ogni misura è la "scheda di lettura" SunPilot: metrica, valore, unità, fase, qualità, timestamp UTC — identica per qualunque vendor.

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/devices/1/measurements?metric=pv_production_power&phase=total&from=2026-07-08T00%3A00%3A00Z&to=2026-07-09T00%3A00%3A00Z&per_page=100" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/devices/1/measurements';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'metric' => 'pv_production_power',
            'phase' => 'total',
            'from' => '2026-07-08T00:00:00Z',
            'to' => '2026-07-09T00:00:00Z',
            'per_page' => '100',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/devices/1/measurements"
);

const params = {
    "metric": "pv_production_power",
    "phase": "total",
    "from": "2026-07-08T00:00:00Z",
    "to": "2026-07-09T00:00:00Z",
    "per_page": "100",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "metric": "pv_production_power",
            "value": 41800,
            "unit": "W",
            "sampling": "instantaneous",
            "phase": "total",
            "quality": "measured",
            "timestamp_utc": "2026-07-09T11:00:00.000000Z",
            "source_driver": "fusionsolar"
        }
    ],
    "links": {
        "first": "https://sunpilot.cloud/api/v1/devices/1/measurements?page=1",
        "last": "https://sunpilot.cloud/api/v1/devices/1/measurements?page=1",
        "prev": null,
        "next": null
    },
    "meta": {
        "current_page": 1,
        "per_page": 50,
        "total": 1
    }
}
 

Request      

GET api/v1/devices/{device_id}/measurements

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

device_id   integer     

ID del device. Example: 1

Query Parameters

metric   string  optional    

Filtra per metrica canonica, snake_case (es. pv_production_power, grid_import_power, load_consumption_power, battery_state_of_charge, voltage, irradiance). Example: pv_production_power

phase   string  optional    

Fase: total, l1, l2, l3. Example: total

from   string  optional    

Da (data/ora ISO 8601). Example: 2026-07-08T00:00:00Z

to   string  optional    

A (data/ora ISO 8601). Example: 2026-07-09T00:00:00Z

per_page   integer  optional    

Risultati per pagina (1–200). Default: 50. Example: 100

Allarmi

Allarmi canonici (ADR-027): stato corrente del parco, popolato da sunpilot:scan-alarms. Diverso da GET /alarms (regola "device scollegato" a 30 minuti, non persistita) — qui la soglia è la staleness canonica (6h/24h).

Allarmi live

requires authentication

Regola "device scollegato" a 30 minuti, calcolata live e non persistita. Diversa da GET /alarms/canonical, che usa la soglia di staleness canonica (6h/24h) sullo stato persistito.

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/alarms" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/alarms';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/alarms"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET api/v1/alarms

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Elenca gli allarmi attivi

requires authentication

Solo allarmi correnti (non ancora rientrati), ordinati per severità decrescente (critical prima di warning prima di info).

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/alarms/canonical" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/alarms/canonical';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/alarms/canonical"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Example response (200):


{
    "data": [
        {
            "id": "9c3b1e2a-...",
            "site_id": "7a1f...",
            "site_name": "Villa Bianchi",
            "device_id": "4b2e...",
            "device_name": "Shelly Pro 3EM",
            "code": "device_offline",
            "severity": "warning",
            "status": "raised",
            "message": "Nessuna misura da 9h",
            "raised_at": "2026-07-13T08:00:00.000000Z",
            "last_seen_at": "2026-07-13T11:00:00.000000Z"
        }
    ]
}
 

Request      

GET api/v1/alarms/canonical

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Conferma presa in carico allarme

requires authentication

Example request:
curl --request POST \
    "https://sunpilot.cloud/api/v1/alarms/architecto/acknowledge" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"note\": \"b\",
    \"maintenance_log_id\": \"a4855dc5-0acb-33c3-b921-f4291f719ca0\"
}"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/alarms/architecto/acknowledge';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'note' => 'b',
            'maintenance_log_id' => 'a4855dc5-0acb-33c3-b921-f4291f719ca0',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/alarms/architecto/acknowledge"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "note": "b",
    "maintenance_log_id": "a4855dc5-0acb-33c3-b921-f4291f719ca0"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

POST api/v1/alarms/{alarm_id}/acknowledge

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

alarm_id   string     

The ID of the alarm. Example: architecto

Body Parameters

note   string  optional    

Must not be greater than 2000 characters. Example: b

maintenance_log_id   string  optional    

Must be a valid UUID. Must match an existing stored value. Example: a4855dc5-0acb-33c3-b921-f4291f719ca0

Risolvi allarme

requires authentication

Example request:
curl --request POST \
    "https://sunpilot.cloud/api/v1/alarms/architecto/resolve" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"note\": \"b\",
    \"maintenance_log_id\": \"a4855dc5-0acb-33c3-b921-f4291f719ca0\"
}"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/alarms/architecto/resolve';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'json' => [
            'note' => 'b',
            'maintenance_log_id' => 'a4855dc5-0acb-33c3-b921-f4291f719ca0',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/alarms/architecto/resolve"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "note": "b",
    "maintenance_log_id": "a4855dc5-0acb-33c3-b921-f4291f719ca0"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Request      

POST api/v1/alarms/{alarm_id}/resolve

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

alarm_id   string     

The ID of the alarm. Example: architecto

Body Parameters

note   string  optional    

Must not be greater than 2000 characters. Example: b

maintenance_log_id   string  optional    

Must be a valid UUID. Must match an existing stored value. Example: a4855dc5-0acb-33c3-b921-f4291f719ca0

Diagnostica

Dashboard diagnostica di portfolio: triage a scala su migliaia di impianti, con filtri, ordinamento e aggregati calcolati lato SQL sui campi persistiti da ScanDiagnosticsForSite (nessuna fetch live di PlantDiagnostics::analyze() per riga — vedi design doc 2026-07-24-diagnostica-portfolio-dashboard-design.md).

Elenca gli impianti con la loro diagnostica precalcolata

requires authentication

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/diagnostics?severity=critical&search=Rossi&sort=savings&per_page=25" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/diagnostics';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
        'query' => [
            'severity' => 'critical',
            'search' => 'Rossi',
            'sort' => 'savings',
            'per_page' => '25',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/diagnostics"
);

const params = {
    "severity": "critical",
    "search": "Rossi",
    "sort": "savings",
    "per_page": "25",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET api/v1/diagnostics

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

severity   string  optional    

Filtro esatto: critical|warning|info|ok. Example: critical

search   string  optional    

Ricerca case-insensitive sul nome impianto. Example: Rossi

sort   string  optional    

severity (default, critici prima) | savings (risparmio annuo desc, null in coda) | score (punteggio peggiore prima, null in coda). Example: savings

per_page   integer  optional    

Risultati per pagina (1–200). Default: 25. Example: 25

Diagnostica del sito

requires authentication

Diagnostica installatore di un impianto: performance vs atteso, autoconsumo/ autosufficienza, dimensionamento FV/batteria + upsell, anomalie, risparmio/CO2.

È una vista DERIVATA su una finestra di 1 anno (aggrega decine di migliaia di righe): pesante da ricalcolare a ogni apertura e non real-time. La cachiamo per un'ora — le misure nuove si riflettono entro la finestra, come per la cache PVGIS. Chiave per-sito (id UUID globale, nessuna collisione cross-tenant).

Example request:
curl --request GET \
    --get "https://sunpilot.cloud/api/v1/sites/architecto/diagnostics" \
    --header "Authorization: Bearer {ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
$client = new \GuzzleHttp\Client();
$url = 'https://sunpilot.cloud/api/v1/sites/architecto/diagnostics';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {ACCESS_TOKEN}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
const url = new URL(
    "https://sunpilot.cloud/api/v1/sites/architecto/diagnostics"
);

const headers = {
    "Authorization": "Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());

Request      

GET api/v1/sites/{site_id}/diagnostics

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

site_id   string     

The ID of the site. Example: architecto