Connect API - Device Onboarding
TL;DR
GET /vendors→GET /vendors/{id}/connection-options→ run the OAuth/form flow →POST /vendors/{id}/connections→GET /vendors/{id}/devices→POST /devices- The
flowfield tells you which auth to implement:oidcProxy(WebView + intercept redirect),oidc(PKCE + deep link),ocpp(user pastes URL in wallbox),push(user pastes URL in vendor portal),form(text/secret fields).flowis returned per vendor onGET /vendorsand again onGET /connection-options— callconnection-optionsfor the form/login details, not just to learn the flow type GET /vendors/{id}/devicesreturns VendorDevices — link candidates read live from the vendor, no clever-PV id. Already linked devices can still appear withavailable: false. They are not the same thing as the Devices returned byGET /devicesafter linking. See Concept: VendorDevice vs. Device- After
POST /devices(linking), the device appears inGET /deviceswith itscapabilities[]— that's where real interaction starts
Complete flow for connecting vendor devices via the Connect API.
Just browsing the catalog? The vendor/model catalog is also reachable without authentication via the Public Vendor Catalog — authenticate to get your app-scoped catalog, or call anonymously for the default clever-PV catalog. Onboarding itself still requires the authenticated flow below.
Don't want to implement vendor login yourself? Hand the browser to Connect UI instead of steps 2–4, then list vendor devices and link them. OCPP wallboxes and push meters stay on the dedicated pages below.
Step 1: List Vendors
Code
Response:
Code
Note:
flowis a non-nullable string per vendor. Values:form | oidcProxy | oidc | ocpp | push. The vendor → flow mapping is not contractual — re-read it on every onboarding attempt.
Step 2: Get Connection Options
Code
Response Structure:
Code
Note: Enum values are serialized as camelCase strings.
When to use which flow
You don't choose the flow — the vendor does. You receive it on GET /vendors (per vendor) and again on GET /connection-options. Your job is to implement the five flow types so your client can handle whichever vendor the user picks.
| Flow | Used by (typical) | What the client must implement | Why this flow |
|---|---|---|---|
oidcProxy | Tesla, Volkswagen, Home Connect | A WebView that opens data.url, intercepts the redirect URL containing data.redirect, and POSTs the full URL back. No token handling on the client. | Vendor requires a confidential OAuth client; clever-PV holds the secret and exchanges the code server-side. |
oidc | Viessmann | Standard OIDC with PKCE: generate code_verifier/code_challenge, register the deep-link com.cleverpv.cleverpv://oauthredirect/, exchange code yourself. | Vendor allows a public OAuth client; PKCE keeps it secure without a server secret. |
ocpp | Wallboxes (any OCPP-1.6/2.0.1 hardware) | Display a generated OCPP server URL to the user; they paste it into the wallbox's own configuration UI. No browser flow. | OCPP is a wallbox-to-backend protocol — the wallbox itself initiates the connection. |
push | Push-based electric meters | After POST /connections, read flowOptions[name="pushUrl"] from the response and display it to the user; they paste it into their vendor portal. No browser flow, no client secret. See the dedicated Push Meter Onboarding page. | Vendor-initiated server-push — the vendor portal posts telemetry to the URL on its own cadence; no client secret leaves the server. |
form | Shelly, Vaillant (and others without OAuth) | Render the returned formOptions (text/secret fields or a vendor consent WebView) and POST the collected values as data. | Vendor has no OAuth at all — either a static API key/serial number, or a vendor-specific consent page that returns parameters in the redirect URL. |
Decision rules for client code:
- Always branch on
flow. Never assume a vendor uses a specific flow — vendors can change, and new vendors can be added at any time without a Connect-API version bump. oidcProxyandoidclook similar but are NOT interchangeable.oidcProxyrequires forwarding the full redirect URL;oidcrequires you to extract the code yourself and send it together with the originalcode_verifier.ocpphas no user-facing browser step. If you build an in-app onboarding wizard, theocppbranch must show instructions and the OCPP URL — not a WebView.pushalso has no browser step. The URL is not informOptions— it is returned in theflowOptionsarray of the create-connection response (see Step 4). Display the URL, instruct the user to paste it into the vendor portal.- For
form, inspectformOptions[].typebefore rendering:textandsecretare simple inputs,loginis a WebView whose intercepted redirect URL you forward unchanged asdata.url(see Option D below).
The vendor IDs returned by GET /vendors are stable, but vendor → flow mapping is not contractual. Always re-fetch /connection-options per onboarding attempt.
Step 3: OAuth Flow
Based on the flow value, execute the appropriate OAuth flow:
Flow Types:
oidcProxy: OAuth flow proxied through Clever PV servers. User authenticates via vendor's OAuth page, Clever PV handles token exchange.oidc: Direct OpenID Connect flow.ocpp: Open Charge Point Protocol flow for wallboxes. User manually enters OCPP server URL in their device configuration.push: Vendor-initiated server-push for electric meters. User manually enters a generated push URL in the vendor portal. URL + API key are returned in the create-connection response (flowOptions).form: Non-OAuth authentication using vendor-specific forms (text fields, secrets) or vendor consent pages.
Option A: "oidcProxy"
Example Response from Get Connection Options (here: Tesla):
Code
Flow:
- Open
data.urlin WebView (replace{{ locale }}with e.g.de) - User authenticates with vendor
- Detect and intercept redirection in WebView: URL contains
data.redirectstring - Store the intercepted URL (including the code) in memory for usage in the "Create Connection" step
Redirect Detection:
Code
Example Redirect URL:
Code
Option B: "oidc" (Direct OIDC with PKCE)
For this flow you have to register the redirect Url com.cleverpv.cleverpv://oauthredirect/ as a deep link handler.
Notice: The
redirectUrlis predefined by Clever PV and cannot be customized. You must use the redirect URL returned by the API (com.cleverpv.cleverpv://oauthredirect/) this might lead to conflicting deep link handling, when the user also has the clever-PV app installed on the same device.
Example Response (here: Viessmann):
Code
Flow:
- Fetch OIDC config from
discoveryUrl - Generate PKCE:
code_verifier(random 64 chars) →code_challenge(SHA256 + Base64URL) - Build authorization URL with
code_challenge - Register deep link handler for
redirectUrlscheme - User authenticates, app receives deep link with
code
PKCE Generation:
Code
Authorization URL:
Code
Redirect Detection:
Code
Option C: "ocpp" (OCPP Wallbox Protocol)
For OCPP-enabled wallboxes, the API provides a unique OCPP server URL that the user must manually configure in their wallbox device.
Example Response:
Code
Flow:
- Create the connection first:
POST /vendors/{vendorId}/connectionswithtype: "ocpp" - Read the stable URL from
flowOptions[name="ocppUrl"]in the create response - Display that URL to the user and instruct them to open their wallbox's configuration interface
- User enters the OCPP URL in the wallbox settings, saves and restarts the wallbox (if required)
- Wallbox connects to Clever PV's OCPP endpoint
- Device becomes available in "List Devices of Connection" step
Note: The URL shown by
GET /connection-options(example above) is a randomized preview — its device tag changes on every read. Always use theflowOptions[name="ocppUrl"]URL from the connection, which is stable from creation onward (also re-surfaced onGET /vendors/connections).
Note: The wallbox will not appear in the devices list until it successfully connects to the OCPP endpoint for the first time.
Option D: "form" (Vendor-Specific Forms)
For vendors without OAuth, the API returns form fields to collect. Field types:
"text": Simple text input (e.g., serial number)"secret": Password input (masked, e.g., API key)"login": WebView-based consent flow (not OAuth)
Example Response (Text Field):
Code
Text Field Flow:
- Display input field for
data.label - User enters value
- POST with collected data as shown in the Create Connection - For "form" sub-section.
Example Response (Login Field):
Code
Login Field Workflow:
The "login" field resembles a redirect-based flow similar to OAuth, but it's not standard OIDC/OAuth2. Instead, it's a vendor-specific flow. When a login field is present, it's always the only field in the array.
- Replace placeholders in
data.url(see below), then open it in a WebView (vendor's consent/login page) - User authenticates or grants consent on vendor's page
- Vendor redirects to URL containing
data.redirectstring - Intercept the redirect and forward the complete redirect URL, unchanged (including its query string), as
data.urlin the POST request withtype: "form"— the same pattern as theoidcProxyflow. The Connect API parses the vendor-specific query parameters server-side.
Important: This is not OAuth2/OIDC - there's no authorization code exchange, no tokens. The redirect URL simply contains vendor-specific parameters (like gateway IDs, consent status). Do not parse them out and send them as individual fields — send the full URL as
data.url, otherwise the request fails with401 invalidCredentials.
Placeholders in
data.url: The login URL may contain{state}— a blank the server intentionally does not fill in. Filling it is your job before you open the URL in the WebView. Any random string works: the Connect API never looks at the value. Its only purpose is that the vendor attaches the same value to the redirect URL again, so you can check that the callback really belongs to the login you started. Don't confuse it with{{ locale }}(see Option A): double braces are the server's own placeholder system, which normally arrives already resolved —localeis the only one the client replaces there too.
Example:
Code
Option E: "push" (Push API for Electric Meters)
For push-based electric meters, the API generates a unique push URL on connection creation — not in the connection-options response. The user pastes that URL into their vendor portal so the vendor can post telemetry to Clever PV.
Example Response (from GET /connection-options):
Code
Note:
formOptionsis intentionally empty — the URL is minted byPOST /connections(see Step 4: For"push") and returned insideflowOptions.
Flow:
- Call
POST /connectionswith{ "type": "push" } - Read
flowOptions[name="pushUrl"].datafrom the response and display it - Instruct the user to open their vendor portal and paste the URL into the push/webhook URL field
- User saves the configuration
- Vendor starts posting telemetry at its own cadence
- Device becomes available immediately in Step 7 — you do not need to wait for the first push
See also: Push Meter Onboarding for a complete step-by-step walkthrough with real example payloads.
Step 4: Create Connection
Heads-up — two unrelated
typefields: Thetypediscriminator in the request body below must be one ofform,oidcProxy,oidc,ocpp,push(the onboarding flows fromDeviceOnboardingFlow).
Thetypeyou may have seen insideformOptions[](e.g.text,secret,login) is a form-field type and is unrelated to the connection request — never use it as the requesttype.
Heads-up —
flowOptionson the response: Some flows return a genericflowOptionsarray on the connection response — a list of{name, data}pairs surfacing flow-specific values the client needs after the connection exists. Todaypushpopulates it (withpushUrlandapiKey) andocpppopulates it (withocppUrl). Match entries byname; order is not guaranteed.
For "oidcProxy":
Code
For "oidc":
Code
Note: Field is named
codeChallengebut expects thecode_verifiervalue.
For "ocpp":
Code
Response:
Code
Note: Read
flowOptions[name="ocppUrl"]from this response (or re-fetched viaGET /vendors/connections) — don't compose the URL fromconnection-optionsyourself. Only one pending OCPP connection is allowed at a time per user. If the wallbox doesn't connect, delete the connection before creating a new one.
For "push":
Code
Response:
Code
Note: The push flow needs no
datapayload — the connection itself mints the credential. ReadpushUrlandapiKeyfrom theflowOptionsarray (match byname, not by index). Multiple open push connections per vendor are allowed.
For "form":
Code
Or, for a login field, with the full intercepted redirect URL (do not send parsed query parameters as individual fields):
Code
Response:
Code
Step 5: Delete Connection (Optional)
Currently only supported for specific flow. Support for missing flows will be implemented soon.
For OCPP, this is mandatory cleanup when a wallbox fails to connect — only one pending OCPP connection is allowed per user. For Push, deletion is purely optional; multiple open push connections are allowed, so you only need to delete to revoke an API key.
Code
Response: 204 No Content on success
Important: All devices attached to the connection must be deleted first (via
DELETE /v1/users/{userId}/devices/{deviceId}— see Step 10: Delete Device). If you attempt to delete a connection with linked devices, the endpoint will return an error.
Use cases:
- Wallbox failed to connect to the OCPP endpoint
- User entered incorrect configuration
- Need to create a new OCPP connection (must delete the existing one first)
Step 6: List User Connections (Optional)
Code
Response:
Code
Note:
flowOptionsis re-surfaced on every list call for flows that need it. Todaypushpopulates it (pushUrl,apiKey) andocpppopulates it (ocppUrl); for other connections the field isnull/ omitted. Match items byname, not by index — order is not guaranteed.
Concept: VendorDevice vs. Device
Step 7 and Step 9 below both return something you might call "a device" — they are not the same resource, and conflating them is the most common source of confusion in this flow.
| VendorDevice (Step 7) | Device (Step 8 / 9) | |
|---|---|---|
| Returned by | GET /vendors/{vendorId}/devices?connectionId= | POST /devices, GET /devices, GET /devices/{deviceId} |
| Identity | No clever-PV id. Identified by externalId (the vendor's own serial number / VIN / gateway id) + the connectionId it was found under | Stable clever-PV id (GUID) — used for every further call (PATCH, PUT /{capability}, DELETE) |
| Where it comes from | Fetched live from the vendor's own API on every call — not stored by clever-PV. Already linked devices can still appear with available: false | A persisted clever-PV resource, created exactly once via POST /devices |
| What it's for | A candidate to show the user so they can pick what to link | The linked, functional device — the only place capabilities[] (telemetry, control, interventions) exist |
Linking one to the other: POST /devices (Step 8) takes the VendorDevice's externalId, its model.id, and the connectionId — that's the entire bridge between the two objects. name/text on the VendorDevice are display suggestions only; they are not copied over automatically, so pass them again in the POST /devices body if you want the same label to stick.
What available: false actually means
The API collapses several distinct situations into a single boolean:
- a Device already exists for this
externalId— in the current home, or in a different home (the same vendor account, and therefore the sameexternalId, can be visible to more than one clever-PV home) - the vendor itself reports the device as offline, on unsupported firmware, or an unsupported model
Note: The response only exposes
available, not the reason behind it — you cannot tell "already linked by you", "already linked under a different home", and "vendor says it's unusable" apart from the VendorDevice alone.
Important:
externalIduniqueness is checked globally across all homes when you callPOST /devices, not just within the current home. If a Device already exists for thatexternalId, the request returns409 ConflictwitherrorCode: DUPLICATE_DEVICE, including if it was linked between yourGETand yourPOST. This error describes a duplicate, not every cause ofavailable: false.
Firmware and model compatibility
Devices whose models are available for selection in your app's catalog can still be returned with available: false if the vendor reports incompatible firmware or an unsupported model. Devices whose models are missing from that catalog or cannot be selected are omitted; Step 7 returns 206 Partial Content when such models are found, with only the supported catalog entries in data.
The response does not expose a status, a detailed compatibility reason, or a required firmware version. Do not infer a particular reason from available: false; keep the device unavailable for selection and show a general availability message.
Step 7: List Devices of a Connection
Code
Response:
Code
Step 8: Link Device
Only offer devices with available: true from Step 7 for linking. Availability can change between listing and linking, so a successful list response does not guarantee that the following request will succeed.
Code
Response Similar to result of step "Get Linked Devices"
If linking fails, inspect the HTTP status and error response, including errorCode, and refresh the device list before offering another attempt. Only offer devices that are still listed with available: true, and address the reported error before retrying. 409 Conflict with DUPLICATE_DEVICE means the device is already linked. Other failures can involve authentication or vendor communication; they do not by themselves identify firmware or model incompatibility. There is no single HTTP status for every cause of available: false.
Step 9: Get Linked Devices
Code
Note: For push-style devices, the
connectcapability'sdatacarries aflowOptionsarray mirroring the connection-levelflowOptions(pushUrl,apiKey). OCPP devices behave the same way — theirconnectcapability surfacesocppUrlso you can recover the WebSocket URL from the device alone without re-fetching the connection. See Push Meter Onboarding → Step 7 for the push shape.
Response:
Code
Step 10: Delete Device
Unlinks a device. Some devices run a full vendor facing reset path (eg.: SG-Ready back to Normal if applicable) that honours force. force does ignore errors on some vendors here.
Code
| Parameter | Default | Meaning |
|---|---|---|
force (query) | false | If true, only delete locally even when the vendor reset fails. Honoured by a subset of vendors. |
Responses:
| Status | When |
|---|---|
204 No Content | Deleted (always returned when force=true, unless the device doesn't exist). |
404 Not Found | No such device for this user. |
409 Conflict | force=false and vendor reset failed — device was NOT deleted. |
Errorcodes:
code | Meaning |
|---|---|
connectionExpired | Refresh token invalid. An authenticationRequired intervention is added to the device's core capability. |
deviceUnreachable | Vendor server unreachable. Safe to retry. |
resetFailed | Any other vendor error during reset/delete notification. |
Code
When to use force=true: eg: the vendor account is gone, the hardware is removed, the connection cannot be restored, or the user has confirmed deletion after a 409. Default to force=false and surface the 409 message so the user can make that choice.
Deleting a whole connection: connections cannot be deleted while devices are linked. Delete each device first (Step 10), then the connection (Step 5).