Skip to content
Demo environment: fictional people, companies and prices. Try it as a customer, installer, lender or admin
Jualink

API v1.0.0 · JSON over HTTPS

Build on Jualink

The same sizing engine, marketplace and workflows as the website, for installers' job systems, lenders' loan platforms, monitoring vendors and our own apps.

Base URL
https://jualink.com/api/v1
Endpoints
44 operations in 9 groups
Health
GET /api/v1/health

Start here

Quick start

No key needed for estimates and the public catalogue. Everything else acts for a signed-in person or an organisation's API key.
A solar estimate for a 2-bedroom home in Lagos
curl -X POST https://jualink.com/api/v1/sizing/estimate \
  -H "Content-Type: application/json" \
  -d '{"countryCode":"NG","regionCode":"LA","propertyType":"HOME","backupHours":12,
       "appliances":[{"key":"refrigerator","quantity":1},{"key":"television","quantity":1},
                     {"key":"standing_fan","quantity":4},{"key":"led_bulb","quantity":8},
                     {"key":"wifi_router","quantity":1},{"key":"laptop","quantity":2}]}'
Applications sent to your organisation (financing partner key)
curl https://jualink.com/api/v1/financing/applications?status=SUBMITTED \
  -H "Authorization: Bearer jl_live_XXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

Security

Authentication

Each endpoint below says which of these it accepts.

Signed-in session

For our website and apps. Sign in with POST /api/auth/sign-in/email and send the session cookie back. Requests that change something must carry an Origin header for this site, so other websites can't act for a signed-in visitor.

API key

For an organisation's own systems: Authorization: Bearer jl_live_… (jl_test_… outside production). A key acts as the team member who created it, limited to its scopes. It is shown once; we keep only a hash. Owners and admins create and revoke keys: financing partners at /lender/api, installers at /pro/api.

Device token

For monitoring hardware: Authorization: Device jldev_…. A token can only add readings for the one device it was issued to, and is shown once when the device is registered.

ScopeAllowsWho can hold it
financing:readList and read the applications customers sent to your organisation.Financing partners
financing:writeStart reviews, request information, decline, make offers, mark loans funded and report repayment status. Includes financing:read.Financing partners
projects:readList quote-request leads and the projects your company was awarded.Installers
projects:writeSubmit proposals and record equipment orders, deliveries and serial numbers. Includes projects:read.Installers
monitoring:writeRegister inverter or battery monitors on systems your company installed and receive their ingestion tokens.Installers
catalog:writeReserved for supplier catalogue endpoints, which are not available yet. It can't be granted today.Nobody yet (reserved)

Conventions

Responses, errors and limits

Success
HTTP/1.1 200 OK
Cache-Control: no-store
X-Request-Id: 6f1c…

{ "data": { … }, "meta": { … } }
Error
HTTP/1.1 422 Unprocessable Entity

{ "error": {
    "code": "validation_failed",
    "message": "Some fields are missing or invalid.",
    "fields": { "appliances.0.quantity": "Too small: expected number to be >=1" } } }
  • 400 invalid_json the body isn't JSON
  • 401 unauthenticated no or invalid credentials
  • 403 forbidden wrong account type, missing scope or cross-site request
  • 404 not_found doesn't exist, or isn't yours
  • 409 conflict the record's state doesn't allow it
  • 422 validation_failed see error.fields
  • 429 rate_limited wait for Retry-After seconds
  • 500 internal_error quote the X-Request-Id to support

Limits: 600 requests a minute per person or key, 120 per address for public endpoints, 20 estimates a minute per address and 120 monitoring pushes a minute per device. Bodies up to 1 MB. Lists return everything that belongs to you, except financing applications (the 200 most recently updated).

Endpoints

Platform

Health, identity and reference data.
GET/api/v1/health

Service health

Checks the API and its database. Returns 503 when the database is unavailable.

Public
GET/api/v1/openapi.json

OpenAPI 3.1 document

This API described as OpenAPI 3.1 (JSON Schema 2020-12). The document itself is returned, not wrapped in `data`.

Public
GET/api/v1/me

Who am I

The user (or API key) the request is authenticated as. Any valid API key may call it.

Session or API key
GET/api/v1/countries

Active countries and regions

Countries where the service is live or piloting, with region codes and sun-hour assumptions.

Public
GET/api/v1/appliances

Appliance catalogue

Typical wattages and daily hours. With propertyType, appliances relevant to it come first.

Public

Query parameters

FieldTypeNotes
propertyType"HOME" | "SHOP" | "OFFICE" | "SCHOOL" | "CHURCH" | "MOSQUE" | "CLINIC" | "FARM" | "RESTAURANT" | "HOTEL" | "FACTORY" | "APARTMENT_BUILDING" | "OTHER"Optional

Endpoints

Sizing

Stateless solar estimates from the same engine as the website.
POST/api/v1/sizing/estimate

Estimate a solar system

Runs the sizing engine with the country's configured prices and the region's sun hours. Nothing is stored. Mix catalogue items ({ key, quantity }) and custom ones ({ name, watts, quantity, hoursDay, hoursNight }).

Public

Request body (JSON)

FieldTypeNotes
countryCode*string, 2+ chars, ≤ 2 charsISO code of an active country, e.g. NG
regionCode*string, 1+ chars, ≤ 10 charsRegion code from GET /countries, e.g. LA for Lagos
propertyType*"HOME" | "SHOP" | "OFFICE" | "SCHOOL" | "CHURCH" | "MOSQUE" | "CLINIC" | "FARM" | "RESTAURANT" | "HOTEL" | "FACTORY" | "APARTMENT_BUILDING" | "OTHER"Required
appliances*object[]Required
appliances[]: a catalogue appliance
appliances[].key*string, 1+ chars, ≤ 60 charsAppliance key from GET /appliances
appliances[].quantity*integer, ≥ 1, ≤ 500Required
appliances[].hoursDaynumber, ≥ 0, ≤ 12Hours used 06:00–18:00; defaults to the catalogue value
appliances[].hoursNightnumber, ≥ 0, ≤ 12Hours used 18:00–06:00; defaults to the catalogue value
appliances[].isCriticalbooleanKeep running in the Essential option; defaults to the catalogue value
appliances[]: or an appliance of your own
appliances[].name*string, 1+ chars, ≤ 80 charsRequired
appliances[].watts*integer, ≥ 1, ≤ 100000Running watts per unit
appliances[].quantity*integer, ≥ 1, ≤ 500Required
appliances[].hoursDay*number, ≥ 0, ≤ 12Required
appliances[].hoursNight*number, ≥ 0, ≤ 12Required
appliances[].surgeFactornumber, ≥ 1, ≤ 8Optional
appliances[].dutyCyclenumber, ≥ 0.05, ≤ 1Optional
appliances[].isCriticalbooleanOptional
appliances[].isMotorLoadbooleanOptional
appliances[].isThreePhasebooleanOptional
backupHoursinteger, ≥ -9007199254740991, ≤ 9007199254740991Optional
batteryPreference"NO_PREFERENCE" | "LITHIUM_LFP" | "LEAD_ACID"Optional
hasThreePhaseSupplybooleanOptional

Endpoints

Assessments

Saved energy assessments for signed-in customers.
POST/api/v1/assessments

Start an assessment

Signed-in sessionRoles: CUSTOMER201 Created

Request body (JSON)

FieldTypeNotes
countryId*string (uuid)Required
regionId*string (uuid)Required
city*string, 2+ chars, ≤ 80 charsRequired
latitudenumber, ≥ -90, ≤ 90Optional
longitudenumber, ≥ -180, ≤ 180Optional
propertyType*"HOME" | "SHOP" | "OFFICE" | "SCHOOL" | "CHURCH" | "MOSQUE" | "CLINIC" | "FARM" | "RESTAURANT" | "HOTEL" | "FACTORY" | "APARTMENT_BUILDING" | "OTHER"Required
bedroomsinteger, ≥ 0, ≤ 50Optional
businessNamestring, ≤ 120 charsOptional
GET/api/v1/assessments/{id}

Get an assessment

Signed-in sessionRoles: CUSTOMER, ADMIN, SUPER_ADMIN

Path parameters

FieldTypeNotes
id*string (uuid)Required
PUT/api/v1/assessments/{id}/appliances

Replace the appliance list

Signed-in sessionRoles: CUSTOMER

Path parameters

FieldTypeNotes
id*string (uuid)Required

Request body (JSON)

FieldTypeNotes
appliances*object[]Required
appliances[].applianceIdstring (uuid) | nullOptional
appliances[].name*string, 1+ chars, ≤ 80 charsRequired
appliances[].quantity*integer, ≥ 1, ≤ 500Required
appliances[].watts*integer, ≥ 1, ≤ 100000Required
appliances[].hoursDay*number, ≥ 0, ≤ 12Required
appliances[].hoursNight*number, ≥ 0, ≤ 12Required
appliances[].surgeFactornumber, ≥ 1, ≤ 8Optional
appliances[].dutyCyclenumber, ≥ 0.05, ≤ 1Optional
appliances[].isCriticalbooleanOptional
appliances[].isMotorLoadbooleanOptional
appliances[].isThreePhasebooleanOptional
PUT/api/v1/assessments/{id}/usage

Save backup and usage answers

Signed-in sessionRoles: CUSTOMER

Path parameters

FieldTypeNotes
id*string (uuid)Required

Request body (JSON)

FieldTypeNotes
backupHoursDesired*integer, ≥ -9007199254740991, ≤ 9007199254740991Required
batteryPreference"NO_PREFERENCE" | "LITHIUM_LFP" | "LEAD_ACID"Optional
gridHoursPerDaynumber, ≥ 0, ≤ 24Optional
hasGeneratorbooleanOptional
generatorLitresPerWeeknumber, ≥ 0, ≤ 10000Optional
monthlyFuelSpendnumber, ≥ 0Optional
monthlyGridSpendnumber, ≥ 0Optional
hasThreePhaseSupplybooleanOptional
operatingHoursStartinteger, ≥ 0, ≤ 23Optional
operatingHoursEndinteger, ≥ 0, ≤ 24Optional
operatingDaysPerWeekinteger, ≥ 1, ≤ 7Optional
expansionPlansstring, ≤ 500 charsOptional
POST/api/v1/assessments/{id}/complete

Run the sizing engine and store the three options

Signed-in sessionRoles: CUSTOMER

Path parameters

FieldTypeNotes
id*string (uuid)Required
POST/api/v1/assessments/{id}/select-tier

Choose Essential, Recommended or Maximum

Signed-in sessionRoles: CUSTOMER

Path parameters

FieldTypeNotes
id*string (uuid)Required

Request body (JSON)

FieldTypeNotes
tier*"ESSENTIAL" | "RECOMMENDED" | "MAXIMUM"Required

Endpoints

Catalogue

Verified products and installers. Public.
GET/api/v1/products

Verified products

Only products whose documentation the platform team reviewed (verificationStatus VERIFIED).

Public

Query parameters

FieldTypeNotes
category"SOLAR_PANEL" | "INVERTER" | "BATTERY" | "CHARGE_CONTROLLER" | "SOLAR_GENERATOR" | "MOUNTING" | "CABLE" | "PROTECTION" | "MONITORING" | "PACKAGE" | "ACCESSORY"Optional
qstring, 2+ chars, ≤ 80 charsSearch in name, model and manufacturer
countrystring, 2+ chars, ≤ 2 charsOnly prices offered in this country
GET/api/v1/products/{slug}

A verified product with supplier offers

Public

Path parameters

FieldTypeNotes
slug*string, 1+ chars, ≤ 160 charsRequired
GET/api/v1/installers

Verified installers

Public

Query parameters

FieldTypeNotes
countrystring, 2+ chars, ≤ 2 charsOptional
regionstring, 1+ chars, ≤ 10 charsRegion code; needs country
GET/api/v1/installers/{slug}

A verified installer's public profile

Public

Path parameters

FieldTypeNotes
slug*string, 1+ chars, ≤ 160 charsRequired

Endpoints

Customer

Quote requests, projects, payments, warranties, maintenance and notifications for a signed-in customer.
POST/api/v1/quote-requests

Ask matched installers for proposals

Needs a completed assessment with a chosen option. consentToShare must be true: it records the customer's consent to share the assessment with matched installers.

Signed-in sessionRoles: CUSTOMER201 Created

Request body (JSON)

FieldTypeNotes
assessmentId*string (uuid)Required
addressLine1*string, 3+ chars, ≤ 160 charsRequired
addressLine2string, ≤ 160 charsOptional
landmarkstring, ≤ 160 charsOptional
preferredStartDatestring (date-time)Optional
wantsFinancingbooleanOptional
budgetMaxnumberOptional
notesstring, ≤ 2000 charsOptional
consentToShare*trueRequired
GET/api/v1/quote-requests

My quote requests

Signed-in sessionRoles: CUSTOMER
GET/api/v1/quote-requests/{id}

Compare proposals for a request

Signed-in sessionRoles: CUSTOMER, ADMIN, SUPER_ADMIN

Path parameters

FieldTypeNotes
id*string (uuid)Required
POST/api/v1/quotes/{id}/accept

Accept a proposal and start the project

Signed-in sessionRoles: CUSTOMER201 Created

Path parameters

FieldTypeNotes
id*string (uuid)Required
GET/api/v1/projects

My projects

Signed-in sessionRoles: CUSTOMER
GET/api/v1/projects/{id}

A project I'm part of

For the customer, the awarded installer (session or projects:read key) and staff. Customer contact details are shown to the installer; financing details only to the customer and staff.

Session or API keyRoles: CUSTOMER, INSTALLER, TECHNICIAN, ADMIN, SUPER_ADMINKey scope: projects:read

Path parameters

FieldTypeNotes
id*string (uuid)Required
GET/api/v1/payments

My payments

Signed-in sessionRoles: CUSTOMER
GET/api/v1/warranties

Warranties on my systems

Signed-in sessionRoles: CUSTOMER
POST/api/v1/warranty-claims

Claim under a warranty

Signed-in sessionRoles: CUSTOMER201 Created

Request body (JSON)

FieldTypeNotes
warrantyId*string (uuid)Required
issue*string, 20+ chars, ≤ 3000 charsRequired
occurredAtstring (date-time)Optional
GET/api/v1/maintenance-requests

My maintenance requests

Signed-in sessionRoles: CUSTOMER
POST/api/v1/maintenance-requests

Request a maintenance visit

Signed-in sessionRoles: CUSTOMER201 Created

Request body (JSON)

FieldTypeNotes
passportId*string (uuid)Required
type*"SYSTEM_INSPECTION" | "PANEL_CLEANING" | "BATTERY_REPLACEMENT" | "INVERTER_REPAIR" | "SYSTEM_UPGRADE" | "TROUBLESHOOTING" | "PREVENTIVE_MAINTENANCE"Required
description*string, 10+ chars, ≤ 3000 charsRequired
urgency"LOW" | "NORMAL" | "HIGH"Optional
preferredDatestring (date-time)Optional
GET/api/v1/notifications

My notifications

Available to every signed-in user, newest first.

Signed-in session

Query parameters

FieldTypeNotes
limitinteger, ≥ 1, ≤ 100Optional
POST/api/v1/notifications/read

Mark notifications read

Signed-in session

Request body (JSON)

FieldTypeNotes
idstring (uuid)Mark one notification; omit to mark all as read

Endpoints

Installer

Leads, proposals and project updates for installer companies.
GET/api/v1/leads

Quote-request leads for my company

Session or API keyRoles: INSTALLER, TECHNICIANKey scope: projects:read
GET/api/v1/leads/{invitationId}

A lead with the customer's assessment

Opening a lead marks it viewed. The customer is identified by first name until they accept a proposal.

Session or API keyRoles: INSTALLER, TECHNICIANKey scope: projects:read

Path parameters

FieldTypeNotes
invitationId*string (uuid)Required
POST/api/v1/leads/{invitationId}/quote

Send or revise a proposal

Only verified installers. Totals and tax are computed server-side from the items.

Session or API keyRoles: INSTALLERKey scope: projects:write201 Created

Path parameters

FieldTypeNotes
invitationId*string (uuid)Required

Request body (JSON)

FieldTypeNotes
panelArrayKw*number, ≤ 10000Required
batteryKwh*number, ≥ 0, ≤ 100000Required
batteryChemistry*"LITHIUM_LFP" | "LEAD_ACID"Required
inverterKva*number, ≤ 10000Required
items*object[]Required
items[].variantIdstring (uuid) | nullOptional
items[].category*"SOLAR_PANEL" | "INVERTER" | "BATTERY" | "CHARGE_CONTROLLER" | "SOLAR_GENERATOR" | "MOUNTING" | "CABLE" | "PROTECTION" | "MONITORING" | "PACKAGE" | "ACCESSORY"Required
items[].description*string, 2+ chars, ≤ 200 charsRequired
items[].brandstring, ≤ 80 charsOptional
items[].modelstring, ≤ 80 charsOptional
items[].quantity*number, ≤ 10000Required
items[].unitPrice*number, ≥ 0Required
items[].warrantyYearsinteger, ≥ 0, ≤ 30Optional
labourCost*number, ≥ 0Required
otherCostnumber, ≥ 0Optional
discountnumber, ≥ 0Optional
pricesIncludeTaxbooleanOptional
estimatedInstallDatestring (date-time)Optional
installDurationDaysinteger, ≥ 1, ≤ 365Optional
validDaysinteger, ≥ 3, ≤ 90Optional
workmanshipWarrantyMonths*integer, ≥ 0, ≤ 240Required
maintenancePackagestring, ≤ 500 charsOptional
maintenanceIncludedMonthsinteger, ≥ 0, ≤ 120Optional
notesstring, ≤ 3000 charsOptional
GET/api/v1/installer/projects

Projects awarded to my company

Session or API keyRoles: INSTALLER, TECHNICIANKey scope: projects:read
POST/api/v1/projects/{id}/equipment-ordered

Mark equipment ordered

Session or API keyRoles: INSTALLER, TECHNICIANKey scope: projects:write

Path parameters

FieldTypeNotes
id*string (uuid)Required

Request body (JSON)

FieldTypeNotes
notesstring, ≤ 1000 charsOptional
POST/api/v1/projects/{id}/equipment-delivered

Mark equipment delivered

Session or API keyRoles: INSTALLER, TECHNICIANKey scope: projects:write

Path parameters

FieldTypeNotes
id*string (uuid)Required

Request body (JSON)

FieldTypeNotes
notesstring, ≤ 1000 charsOptional
POST/api/v1/projects/{id}/equipment

Record installed equipment serial numbers

After installation has started. A serial already recorded on another system is refused.

Session or API keyRoles: INSTALLER, TECHNICIANKey scope: projects:write

Path parameters

FieldTypeNotes
id*string (uuid)Required

Request body (JSON)

FieldTypeNotes
items*object[]Required
items[].category*"SOLAR_PANEL" | "INVERTER" | "BATTERY" | "CHARGE_CONTROLLER" | "MONITORING" | "PROTECTION" | "MOUNTING" | "ACCESSORY"Required
items[].manufacturer*string, 1+ chars, ≤ 80 charsRequired
items[].model*string, 1+ chars, ≤ 80 charsRequired
items[].serialNumber*string, 3+ chars, ≤ 80 charsRequired
items[].capacityLabelstring, ≤ 40 charsOptional
items[].warrantyYearsinteger, ≥ 0, ≤ 30Optional
items[].variantIdstring (uuid)Optional

Endpoints

Financing

Applications sent to a financing partner, and the partner's decisions.
GET/api/v1/financing/applications

Applications sent to my organisation

At most 200, most recently updated first. meta.counts has the pipeline counts per status.

Session or API keyRoles: FINANCIERKey scope: financing:read

Query parameters

FieldTypeNotes
status"SUBMITTED" | "UNDER_REVIEW" | "INFO_REQUIRED" | "APPROVED" | "REJECTED" | "OFFER_ACCEPTED" | "FUNDED" | "REPAYMENT_ACTIVE" | "COMPLETED" | "DELINQUENT" | "WITHDRAWN"Optional
GET/api/v1/financing/applications/{id}

One application

Identity and bank numbers are masked unless ?reveal=true, which decrypts them while the customer's consent is active and writes an audit entry.

Session or API keyRoles: FINANCIERKey scope: financing:read

Path parameters

FieldTypeNotes
id*string (uuid)Required

Query parameters

FieldTypeNotes
reveal"0" | "1" | "true" | "false"true returns decrypted identity and bank details (only while the customer's consent is active); every reveal is audited
POST/api/v1/financing/applications/{id}/decision

Review, request information, decline or make an offer

Body by action: {action:"start_review"} | {action:"request_info", message} | {action:"reject", reason} | {action:"offer", amount, termMonths, annualRatePct, interestMethod, upfrontFees?, monthlyFee?, conditions?, expiresInDays?}.

Session or API keyRoles: FINANCIERKey scope: financing:write

Path parameters

FieldTypeNotes
id*string (uuid)Required

Request body (JSON)

FieldTypeNotes
when action is "start_review"
when action is "request_info"
message*string, 5+ chars, ≤ 2000 charsRequired
when action is "reject"
reason*string, 3+ chars, ≤ 2000 charsRequired
when action is "offer"
amount*numberRequired
termMonths*integer, ≥ 1, ≤ 120Required
annualRatePct*number, ≥ 0, ≤ 200Required
interestMethod*"REDUCING_BALANCE" | "FLAT"Required
upfrontFeesnumber, ≥ 0Optional
monthlyFeenumber, ≥ 0Optional
conditionsstring, ≤ 2000 charsOptional
expiresInDaysinteger, ≥ 1, ≤ 60Optional
POST/api/v1/financing/applications/{id}/funded

Mark an accepted offer funded

The disbursed amount must match the accepted offer. Records the disbursement against the project's payment schedule.

Session or API keyRoles: FINANCIERKey scope: financing:write

Path parameters

FieldTypeNotes
id*string (uuid)Required

Request body (JSON)

FieldTypeNotes
partnerLoanRef*string, 2+ chars, ≤ 80 charsRequired
disbursedAmount*numberRequired
signedAtstring (date-time)Optional
POST/api/v1/financing/applications/{id}/repayment

Report repayment status

Session or API keyRoles: FINANCIERKey scope: financing:write

Path parameters

FieldTypeNotes
id*string (uuid)Required

Request body (JSON)

FieldTypeNotes
status*"REPAYMENT_ACTIVE" | "COMPLETED" | "DELINQUENT"Required
nextPaymentDueAtstring (date-time)Optional
outstandingBalancenumber, ≥ 0Optional
notestring, ≤ 500 charsOptional

Endpoints

Monitoring

Monitoring devices and their readings.
POST/api/v1/monitoring/devices

Register a monitoring device on a Solar Passport

For the system's owner, its installer (session or monitoring:write key) or staff. Returns the device token once; registering the same provider and externalId again rotates it.

Session or API keyRoles: CUSTOMER, INSTALLER, TECHNICIAN, ADMIN, SUPER_ADMINKey scope: monitoring:write201 Created

Request body (JSON)

FieldTypeNotes
provider*stringRequired
externalId*string, 1+ chars, ≤ 120 charsRequired
displayName*string, 2+ chars, ≤ 80 charsRequired
passportId*string (uuid)Required
POST/api/v1/monitoring/readings

Push readings from a device

Authenticate with `Authorization: Device <token>`. Up to 500 readings per call; a reading for a time already stored is counted as a duplicate, so retries are safe. Power in W (gridPowerW + import / − export; batteryPowerW + charging / − discharging), batterySocPct 0–100.

Device token

Request body (JSON)

FieldTypeNotes
readings*object[]Required
readings[].recordedAt*string (date-time)Required
readings[].pvPowerWnumber, ≥ 0, ≤ 2000000Optional
readings[].loadPowerWnumber, ≥ 0, ≤ 2000000Optional
readings[].gridPowerWnumber, ≥ -2000000, ≤ 2000000Optional
readings[].batteryPowerWnumber, ≥ -2000000, ≤ 2000000Optional
readings[].batterySocPctnumber, ≥ 0, ≤ 100Optional
readings[].pvEnergyTodayWhnumber, ≥ 0, ≤ 100000000Optional
readings[].statusCodestring, ≤ 40 charsOptional

Endpoints

Staff

Operations endpoints for platform administrators.
GET/api/v1/admin/kpis

Marketplace KPIs

Signed-in sessionRoles: ADMIN, SUPER_ADMIN

Integrations

Webhooks

Events we receive from providers, and events we send to partners.

Payment providers

Configure these URLs in each provider's dashboard. A webhook that fails verification gets 401 and nothing is recorded. Verified events settle the payment once, however many times they arrive.

  • https://jualink.com/api/webhooks/payments/paystack: signed with HMAC-SHA512 of the body using your secret key (x-paystack-signature).
  • https://jualink.com/api/webhooks/payments/flutterwave: verif-hash must equal FLUTTERWAVE_WEBHOOK_HASH; the transaction is then re-verified with Flutterwave.
  • https://jualink.com/api/webhooks/payments/mpesa?token=…: set automatically as the STK Push callback. Daraja callbacks aren't signed, so each payment gets its own token, checked against the CheckoutRequestID. We answer {"ResultCode":0,"ResultDesc":"Accepted"}.

Events to financing partners

Set an HTTPS endpoint at /lender/api. We POST JSON with ids and statuses only; fetch details with your key. One attempt, 5 second timeout, any 2xx counts as delivered. Retries aren't built yet, so reconcile with GET /financing/applications.

  • application.submitted A customer sent you a new financing application.
  • application.withdrawn The customer withdrew an application (and its data-sharing consent).
  • application.info_provided The customer answered your request for more information.
  • offer.accepted The customer accepted your offer. Mark the loan funded once you disburse.
  • offer.declined The customer declined your offer.
  • ping A test event sent from the API & integrations page.
Request
POST /your/endpoint
Content-Type: application/json
X-Jualink-Event: application.submitted
X-Jualink-Timestamp: 1790000000
X-Jualink-Signature: sha256=5d41…

{ "id": "0199…", "type": "application.submitted",
  "createdAt": "2026-09-26T10:00:00.000Z",
  "data": { "applicationId": "…", "reference": "FIN-2026-…",
            "status": "SUBMITTED", "amountRequested": 3200000,
            "depositAmount": 800000, "termMonths": 24,
            "currency": "NGN", "projectReference": "PRJ-2026-…" } }
Verifying the signature (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the exact request body, before JSON.parse
export function verify(rawBody, headers, secret) {
  const ts = headers["x-jualink-timestamp"];
  const sig = headers["x-jualink-signature"] ?? "";
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`).digest("hex");
  return sig.length === expected.length &&
    timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Your signing secret is shown at /lender/api. It is derived from a platform secret and your partner id, so it can't be rotated on its own yet; per-partner rotation is planned.

WhatsApp estimate assistant

Point the WhatsApp Cloud API webhook at https://jualink.com/api/webhooks/whatsapp. Meta's verification request is answered when hub.verify_token matches WHATSAPP_VERIFY_TOKEN; message deliveries must carry a valid X-Hub-Signature-256 (WHATSAPP_APP_SECRET). The assistant asks what the customer wants to power, the appliances, backup hours and location, shows the three options with prices, and hands over to the website with a private link. Numbers are never stored, only a keyed hash. Replies are sent when WHATSAPP_PROVIDER=meta, and messages are processed only while the whatsapp feature flag is on. Try it in the simulator.

Monitoring

Register a device on a Solar Passport with POST /monitoring/devices, then push readings to POST /monitoring/readings, or let the vendor push its own format to https://jualink.com/api/webhooks/monitoring/<adapter> with the same device token (header, or ?token= if the vendor can only store a URL). Readings are stored as sent and never estimated; a repeated timestamp is a duplicate, so retries are safe. Owners get a "Battery low" alert below 20% (at most daily). Offline alerts are sent by the background job markOfflineDevices() when the worker runs it; the energy view marks a device offline after 60 minutes without data either way. Production accepts readings only with MONITORING_INGEST_ENABLED=true.

Canonical reading
{ "readings": [ {
    "recordedAt": "2026-09-26T12:00:00Z",
    "pvPowerW": 1850,        // solar output
    "loadPowerW": 640,       // consumption
    "gridPowerW": 0,         // + import, − export
    "batteryPowerW": 1150,   // + charging, − discharging
    "batterySocPct": 76.5,
    "pvEnergyTodayWh": 5400  // optional, device's own counter
} ] }

Adapters

  • generic Generic JSON: An array of flat records, or an object with a `readings`/`data` array, or a single record. Field names are matched loosely (e.g. `timestamp`, `pv_power`, `soc`, `pv_power_kw`); kW and kWh fields are converted.

Adding a vendor: write a MonitoringAdapter in src/server/api/monitoring-adapters.ts that turns the vendor's JSON into canonical readings, and register it under a lowercase key. Validation, dedupe and alerts stay shared. No vendor-specific adapters exist yet.

Honest status

What's built and what's planned

Working today

  • Every endpoint on this page. The page and the OpenAPI document are generated from the same definitions the server enforces.
  • API keys with scopes; self-service for financing partners.
  • Paystack, Flutterwave and M-Pesa webhooks.
  • The WhatsApp estimate assistant and its hand-over to the website.
  • Monitoring devices, readings, the generic JSON adapter and battery alerts.
  • Signed partner webhooks, with a test event.

Not yet

  • Webhook retries and per-partner secret rotation.
  • Cursor pagination and idempotency keys (webhook settlement and readings are already idempotent).
  • Vendor-specific monitoring adapters, and supplier catalogue endpoints (catalog:write is reserved).
  • Document downloads with an API key (downloads need a signed-in session).
  • Calls from other websites' browsers: no CORS, the API is for servers and our own pages.