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 — unlinked candidates read live from the vendor, no clever-PV id. 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.
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 with vendor-specific redirect-parameter parsing (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.
- Open
data.urlin WebView (vendor's consent/login page) - User authenticates or grants consent on vendor's page
- Vendor redirects to URL containing
data.redirectstring - Intercept redirect URL and parse query parameters
- Use parsed parameters as
datain POST request withtype: "form"
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) that you forward to the Connect API.
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 with login field redirect parameters:
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. It disappears from the list as soon as it's linked, or removed from the vendor's account | 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 — this is enforced, not just advisory:
externalIduniqueness is checked globally across all homes when you callPOST /devices, not just within the current home. Linking a VendorDevice that isavailable: false— or one that became unavailable between yourGETand yourPOST— returns409 ConflictwitherrorCode: DUPLICATE_DEVICE. It does not return the existing Device or otherwise succeed silently.
Step 7: List Devices of a Connection
Code
Response:
Code
Step 8: Link Device
Code
Response Similar to result of step "Get Linked Devices"
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).