curl --request POST \
--url https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data @- <<EOF
{
"reason": "The device's export capacity is below the program minimum."
}
EOFimport requests
url = "https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll"
payload = { "reason": "The device's export capacity is below the program minimum." }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({reason: 'The device\'s export capacity is below the program minimum.'})
};
fetch('https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'reason' => 'The device\'s export capacity is below the program minimum.'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll"
payload := strings.NewReader("{\n \"reason\": \"The device's export capacity is below the program minimum.\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"reason\": \"The device's export capacity is below the program minimum.\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"reason\": \"The device's export capacity is below the program minimum.\"\n}"
response = http.request(request)
puts response.read_body{
"id": "enr_7c2f1a9e4b0d8f3a6e2c5d10",
"object": "vpp_enrollment",
"live_mode": true,
"device_id": "dev_63a6087272941ef077a8fd3e",
"program_id": "vpr_5c8d1f2a7b3e4d6c9a0f1b2c",
"oem_id": "oem_6a3a8dada72cd0a249ff9528",
"enrollment_state": "ACTIVE",
"enrollment_step": "ACTIVE",
"suspension_reasons": [],
"terms": {
"rewards": [
{
"basis": "FIXED",
"rate": 15,
"period": "QUARTER"
},
{
"basis": "DELIVERED_KWH",
"rate": 0.35
}
],
"opt_out_policy": "ALLOWED",
"notice_days": 30
},
"time_enrolled": "2026-01-24T14:15:22Z",
"time_unenrolled": null,
"time_created": "2026-01-20T09:30:00Z",
"time_updated": "2026-08-12T09:30:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}Unenroll a device
Ends an enrollment on your initiative. At any PENDING step it turns the request to join down, and the enrollment goes REJECTED at step INELIGIBLE. In ACTIVE, SUSPENDED or LEAVING it ends the enrollment at once, along with any commitments the device still holds, and the enrollment goes ENDED. The reason is shown to the customer where the OEM app supports it. A terminal state returns 422 enrollment_ended. Fires vpp_enrollment.update.
curl --request POST \
--url https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data @- <<EOF
{
"reason": "The device's export capacity is below the program minimum."
}
EOFimport requests
url = "https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll"
payload = { "reason": "The device's export capacity is below the program minimum." }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({reason: 'The device\'s export capacity is below the program minimum.'})
};
fetch('https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'reason' => 'The device\'s export capacity is below the program minimum.'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll"
payload := strings.NewReader("{\n \"reason\": \"The device's export capacity is below the program minimum.\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"reason\": \"The device's export capacity is below the program minimum.\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://fleet-api.flatpeak.com/vpp-enrollments/{enrollment_id}/unenroll")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"reason\": \"The device's export capacity is below the program minimum.\"\n}"
response = http.request(request)
puts response.read_body{
"id": "enr_7c2f1a9e4b0d8f3a6e2c5d10",
"object": "vpp_enrollment",
"live_mode": true,
"device_id": "dev_63a6087272941ef077a8fd3e",
"program_id": "vpr_5c8d1f2a7b3e4d6c9a0f1b2c",
"oem_id": "oem_6a3a8dada72cd0a249ff9528",
"enrollment_state": "ACTIVE",
"enrollment_step": "ACTIVE",
"suspension_reasons": [],
"terms": {
"rewards": [
{
"basis": "FIXED",
"rate": 15,
"period": "QUARTER"
},
{
"basis": "DELIVERED_KWH",
"rate": 0.35
}
],
"opt_out_policy": "ALLOWED",
"notice_days": 30
},
"time_enrolled": "2026-01-24T14:15:22Z",
"time_unenrolled": null,
"time_created": "2026-01-20T09:30:00Z",
"time_updated": "2026-08-12T09:30:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}{
"object": "error",
"type": "invalid_request",
"code": "access_denied",
"message": "The requested resource could not be found.",
"context": "api.price_get",
"live_mode": true,
"time_created": "2026-01-01T00:00:00Z"
}Authorizations
Authenticate with the bearer_token obtained from the authentication endpoint.
Body
Optional context for the action.
Response
Enrollment response.
A device's membership of a VPP program. When the program has enrollment.approval_required, requests wait for your decision at AWAITING_APPROVAL. Changes fire vpp_enrollment.update.
The enrollment lifecycle state. enrollment_step gives the detail.
| Value | Meaning |
|---|---|
PENDING | Onboarding: consent, a connected tariff, telemetry, the dispatch test and approval |
ACTIVE | Enrolled and dispatchable |
SUSPENDED | Not dispatchable until every reason in suspension_reasons clears |
LEAVING | A request to leave is waiting for your approval. The device serves only the commitments it already has |
ENDED | Unenrolled. Terminal |
REJECTED | Ineligible, turned down by you, or onboarding failed. Terminal |
PENDING, ACTIVE, SUSPENDED, LEAVING, ENDED, REJECTED "ACTIVE"
The step within enrollment_state.
| Step | State | Meaning |
|---|---|---|
REQUESTED | PENDING | The customer asked to join |
AWAITING_CONSENT | PENDING | Waiting for the consent scopes the program needs |
AWAITING_TARIFF | PENDING | Waiting for a connected tariff at the device's location |
AWAITING_TELEMETRY | PENDING | Waiting for the feeds the program requires |
TESTING_DISPATCH | PENDING | Running the program's dispatch test |
AWAITING_APPROVAL | PENDING | Waiting for your approval |
ACTIVE | ACTIVE | Enrolled |
SUSPENDED | SUSPENDED | Suspended |
UNENROLLING | LEAVING | The customer or the OEM asked to leave. The request waits for your approval |
UNENROLLED | ENDED | Unenrolled |
INELIGIBLE | REJECTED | The device does not qualify, or you unenrolled it while it was PENDING |
FAILED | REJECTED | Onboarding could not complete, for example a failed dispatch test |
REQUESTED, AWAITING_CONSENT, AWAITING_TARIFF, AWAITING_TELEMETRY, TESTING_DISPATCH, AWAITING_APPROVAL, ACTIVE, SUSPENDED, UNENROLLING, UNENROLLED, INELIGIBLE, FAILED "ACTIVE"
Why the enrollment is suspended: an empty list unless enrollment_state is SUSPENDED.
| Value | Meaning |
|---|---|
DISPATCH_UNRESPONSIVE | The device is not acting on instructions |
CONSENT_REVOKED | The customer withdrew a consent scope the program requires |
TELEMETRY_STALLED | A required feed is present but silent beyond its cadence |
USER_PAUSED | Paused by the customer |
DISPATCH_UNRESPONSIVE, CONSENT_REVOKED, TELEMETRY_STALLED, USER_PAUSED Was this page helpful?

