Files

4.8 KiB

SolarEdge internal optimizer API

Status: experimental and unsupported. This document describes calls observed in the SolarEdge Monitoring web portal on 2026-08-09. SolarEdge can change or remove them without notice.

Why a second client is required

The official API at https://monitoringapi.solaredge.com does not expose per-optimizer measurements. Its API key is rejected by the internal portal endpoints with HTTP 401.

The Monitoring portal instead uses an OAuth2 authorization-code flow with PKCE:

  1. Load the hosted SolarEdge/AWS Cognito login form.
  2. Submit the configured portal username and password together with the CSRF token and login cookies.
  3. Exchange the returned authorization code for OAuth tokens.
  4. Exchange those tokens at /services/auth/token?legacy=false for a Monitoring portal session.
  5. Send both the bearer access token and session cookies to internal services.
  6. Refresh the session through POST /services/auth/refresh when necessary.

SolarEdgePortalClient performs this flow with HTTP requests and a memory-only cookie jar. It does not start or depend on a browser. Tokens and cookies are not persisted to disk.

Configuration

Keep all values on the server:

SOLAREDGE_SITE_ID=...
SOLAREDGE_USERNAME=owner@example.com
SOLAREDGE_PASSWORD=...

Do not use the EXPO_PUBLIC_ prefix. Values with that prefix are embedded in the client application and are not secret.

import { createSolarEdgePortalClientFromEnv } from "@solar-dash/solaredgeapi";

const portal = createSolarEdgePortalClientFromEnv();
await portal.authenticate();

Observed endpoints

Logical layout including optimizers

GET /services/layout/logical/generic/v2/site/{siteId}?include-optimizers=true
const layout = await portal.getLogicalLayout();
const optimizerSerials = await portal.listOptimizerSerials();
const optimizerMappings = await portal.listOptimizerMappings();

The response contains the site's logical device tree and optimizer serials. listOptimizerSerials() traverses that tree and returns only nodes whose type is OPTIMIZER. The raw layout is deliberately typed as a generic object because this undocumented structure is more likely to change.

listOptimizerMappings() preserves SolarEdge's logical position fields:

type SolarEdgeOptimizerMapping = {
  serial: string;
  inverterId?: string; // e.g. "1"
  stringId?: string; // e.g. "1.1"
  optimizerId?: string; // e.g. "1.1.16"
  inverterOrder?: number;
  stringOrder?: number;
  optimizerOrder?: number;
};

The IDs come from the portal's displayOrder properties and are therefore preferable to deriving positions from array indexes.

Optimizer information

POST /services/layout/information/optimizers
Content-Type: application/json

["OPTIMIZER_SERIAL_1", "OPTIMIZER_SERIAL_2"]

Observed response container:

type SolarEdgeOptimizerInformation = {
  basicInformationList: Array<{
    serial: string;
    type_?: string;
    model?: string;
    bundleType?: string;
    multiOptimizers?: boolean;
    modules?: Array<{
      orientation?: string;
      tilt?: number;
      azimuth?: number;
      manufacturer?: string;
      model?: string;
    }>;
    [property: string]: unknown;
  }>;
  serialToLiveData: Record<string, {
    lastMeasurement?: string;
    current_A?: number | null;
    optimizerVoltage_V?: number | null;
    power_W?: number | null;
    voltage_V?: number | null;
    total_last_telemetry_energy_WH?: number | null;
    [property: string]: unknown;
  }>;
};
const information = await portal.getOptimizerInformation([
  "OPTIMIZER_SERIAL_1",
  "OPTIMIZER_SERIAL_2",
]);

Optimizer energy graph

GET /services/layout/energy-graph/site/{siteId}/optimizers
  ?chart-time-unit=hours
  &start-date=yyyy-MM-dd
  &end-date=yyyy-MM-dd
  &optimizer-serials=SERIAL_1,SERIAL_2

Observed response:

type SolarEdgeOptimizerEnergy = {
  totalEnergy: number;
  energyBars: Array<{
    measurementTime: string;
    energy: number | null;
  }>;
};
const energy = await portal.getOptimizerEnergy({
  startDate: "2026-08-09",
  endDate: "2026-08-09",
  optimizerSerials: ["OPTIMIZER_SERIAL_1"],
  chartTimeUnit: "hours",
});

Limitations and operational safety

  • The implementation currently supports username/password login without an additional MFA challenge. It throws AUTH_CHALLENGE_REQUIRED if SolarEdge asks for an OTP, authenticator, passkey, or another step.
  • Credentials remain in process memory because they are needed for a fresh login after session expiry. Do not log client options or error request bodies.
  • Use conservative polling and caching. These endpoints have no published rate limits or stability guarantees.
  • Use this only for an account and site you are authorized to access. Review the SolarEdge Monitoring Portal terms before production use.