Juicy Partner Portal

Programmatic battery control,
for optimisers.

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.

How it works

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.

Onboarding

Step 1 — Generate a keypair and CSR

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"

Step 2 — Send us the CSR

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.

Step 3 — Use the signed cert

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

Step 4 — Confirm assignment

We'll let you know once batteries have been assigned to you. Until then, the list will be empty (HTTP 200 with []).

Trust model

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:

Digital signatures Yes

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.

Public Key Infrastructure Yes

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.

Timestamping Partial

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.

Audit logs Partial

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.

Flex plan model

Strategies

StrategyRequired fieldsBehaviour
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).

Limits

Snapshot semantics. Each successful POST entirely replaces the previous plan for that battery. If you want to keep existing blocks, include them in the new submission. The response includes a monotonically-increasing revision number — use it for idempotency checks if you re-send.

Submitting a plan

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"
}

Clearing

DELETE /v1/optimiser/batteries/<battery-id>/flex-plan

Clears the plan and falls the battery back to local control.

Checking current plan (idempotency)

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.

Reading state

EndpointReturns
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.

Error reference

StatusWhen
401No client certificate, or certificate not signed by the Juicy Partner CA, or partner deactivated.
404Battery not found or not assigned to you. We don't distinguish — preventing partner enumeration.
422Plan validation failure (overlapping blocks, past end_time, horizon exceeded, unsupported strategy, power above battery max).
5xxTransient. Retry with exponential backoff.

Operational notes