A REST API for your platform to read state and submit flex plans across the Juicy batteries assigned to you. Mutually-authenticated TLS, snapshot semantics, no surprises.
Juicy assigns specific batteries to your account. You see only those batteries; the API rejects access to anything you don't own.
You control batteries by submitting flex plans — a list of time-bounded blocks describing what the battery should do during each window. The agent on each battery applies the active block; outside any block, it falls back to the customer's local control mode.
Plans are snapshot: every POST .../flex-plan
replaces the previous plan for that battery in full. Agents pick up
changes within ~30 seconds (WebSocket push) and back-stop with a 5-minute
poll, so a missed push never strands a battery in stale steering.
You generate the keypair on your side. Your private key never leaves your infrastructure — non-repudiation is the foundation of the trust model.
# 1. Private key (4096-bit RSA; keep this file safe)
openssl genrsa -out partner.key 4096
# 2. Certificate Signing Request
# Use your company name or service name as the Common Name.
openssl req -new -key partner.key -out partner.csr \
-subj "/CN=your-company"
Email chris@juicy.energy with
partner.csr attached. We'll sign it with
the Juicy Partner CA and return a PEM certificate via a secure channel.
Keep your partner.key private. Never email it. Never check
it in; it's what proves you are you, and we'll keep an audit trail of
actions intiated by this key. - If you think the key has been compromised
contact us immediately and we'll revoke it's access.
Save the cert we send back as partner.crt. Pair it with
your private key on every request:
curl --cert partner.crt --key partner.key \
https://partner.juicy.energy/v1/optimiser/batteries
We'll let you know once batteries have been assigned to you. Until
then, the list will be empty (HTTP 200 with []).
The platform doesn't just prove that a request could have come from you — it ties every action to a specific, named partner certificate whose private key only you hold. Here's how that maps to the four classical non-repudiation primitives:
Every API request runs over mutual TLS. Your private key signs the TLS handshake; the server keeps the negotiated session for the duration of the request. A request without your key cannot be forged, and a request made with your key cannot be disowned without admitting the key was leaked.
We operate a private PKI — the Juicy Partner CA — and sign your X.509 certificate from your CSR. The ingress verifies the cert chain on every connection; the application server then looks up the exact partner identity by certificate fingerprint (SHA-256 of the DER). Revocation is immediate: we mark the cert revoked and subsequent requests are rejected on the next connection. We do not rely on third-party CAs because we want a single, unambiguous root of trust under our operational control.
Every plan submission, cert issuance, and assignment write records a
server-side submitted_at / created_at in
UTC against the Juicy server clock (NTP-synced via GKE). This is
authoritative for our records and for the audit log — but it is not
an RFC 3161 trusted-timestamping signature against a third-party TSA.
If you need cryptographically-bound external timestamps, raise it
with us and we'll add a TSA signer to the pipeline.
Every plan submission, certificate operation, and partner assignment
writes both to the application audit_entries table and
to GCP Cloud Logging (append-only, retained per project policy).
Each plan row stores the SHA-256 fingerprint of the cert that
submitted it, so a forensic timeline ties every steering action to
a specific partner key. The Cloud Logging stream is tamper-evident
at the platform level; the database row is mutable by Juicy
operators with prod write access — we treat the two together as a
balanced compromise between operability and integrity.
| Strategy | Required fields | Behaviour |
|---|---|---|
self_consumption |
— | Battery follows household load: charges from solar excess, discharges to cover consumption, no grid trading. |
fixed_power_setpoint |
setpoint_w (signed) |
Battery holds a fixed AC-side power.
+W = charge, −W = discharge,
0 = stay idle. |
fixed_power_direction |
direction (charge|discharge),
optional max_power_w |
Battery runs at maximum power in the chosen direction
(capped by max_power_w if supplied, else the
battery's rated power). |
end_time_ms must be in the future (60-second
grace for clock skew) and within a 7-day horizon
from now.setpoint_w / max_power_w may not exceed
the battery's max_power_kw × 1000. Read the
battery's limits first via
GET /v1/optimiser/batteries/{id}.revision number — use
it for idempotency checks if you re-send.
POST /v1/optimiser/batteries/<battery-id>/flex-plan
Content-Type: application/json
{
"blocks": [
{
"start_time_ms": 1715600000000,
"end_time_ms": 1715603600000,
"strategy": "self_consumption"
},
{
"start_time_ms": 1715603600000,
"end_time_ms": 1715607200000,
"strategy": "fixed_power_setpoint",
"setpoint_w": -3000
}
]
}
Response:
{
"battery_id": "…",
"revision": 7,
"accepted_block_count": 2,
"effective_at": "2026-05-13T10:15:42Z"
}
DELETE /v1/optimiser/batteries/<battery-id>/flex-plan
Clears the plan and falls the battery back to local control.
GET /v1/optimiser/batteries/<battery-id>/flex-plan
Returns the latest revision plus the full block list. Useful before a resubmit to confirm you're not double-applying.
| Endpoint | Returns |
|---|---|
GET/v1/optimiser/batteries |
Your assigned batteries — id, serial, current SOC and power, status, control mode, max power, etc. |
GET/v1/optimiser/batteries/{id} |
Full detail for one battery. |
GET/v1/optimiser/batteries/{id}/telemetry?hours=N |
Recent raw telemetry rows (up to 168h, capped at 10 000 rows). |
GET/v1/optimiser/batteries/{id}/flex-plan |
The current plan. |
POST/v1/optimiser/batteries/{id}/flex-plan |
Submit / replace the plan. |
DEL/v1/optimiser/batteries/{id}/flex-plan |
Clear the plan. |
| Status | When |
|---|---|
| 401 | No client certificate, or certificate not signed by the Juicy Partner CA, or partner deactivated. |
| 404 | Battery not found or not assigned to you. We don't distinguish — preventing partner enumeration. |
| 422 | Plan validation failure (overlapping blocks, past end_time, horizon exceeded, unsupported strategy, power above battery max). |
| 5xx | Transient. Retry with exponential backoff. |