Two endpoints and one webhook. Built for partner apps like Luminous M-Partner that collect leads in the field and want a designer to take it from there.
Version 1.0 is attached to partner order forms as a schedule. Required fields stay required. v1 only gets additive changes (new optional fields, new event types). Anything that breaks a v1 client ships as v2 with 6 months' notice.
Send Authorization: Bearer <key> on every call. An Admin creates keys in Settings › Integrations › API keys. The key is shown once; SurgePV stores only a hash.
A key belongs to one organisation and acts as that organisation, not as a user. It can call the endpoints on this page and nothing else: no settings, equipment, users or shared reference data. Base URL https://api.surgepv.com/v1, HTTPS only. Sandbox: https://sandbox-api.surgepv.com/v1 with spv_test_ keys.
| Scope | Allows |
|---|---|
projects:create | POST /v1/projects |
proposals:read | GET /v1/projects/{id} and ?external_id= |
60 requests per minute and 5,000 per day, per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Over the limit you get 429 with Retry-After in seconds. Bodies up to 64 KB.
Creates a project in status Customer Inquiry, assigns the next designer by round-robin and applies the design profile set in the mapping. If the address does not geocode, the mobile matches an open project, or the load is over 100 kW, the lead is held for review and no project is made.
POST https://api.surgepv.com/v1/projects Authorization: Bearer spv_live_7Kq2…9f2c Idempotency-Key: MP-48213 Content-Type: application/json { "external_id": "MP-48213", "customer": { "name": "Anil Srivastava", "mobile": "+919415031827", "email": null }, "site": { "address_line_1": "B-14, Sector 21, Indira Nagar", "city": "Lucknow", "state": "Uttar Pradesh", "pincode": "226016", "latitude": null, "longitude": null }, "sanctioned_load_kw": 5, "monthly_bill_inr": 3950, "partner": { "id": "LUM-UP-0417", "name": "Jyoti Power Solutions" } }
HTTP/1.1 201 Created Location: /v1/projects/prj_8f31c2 { "id": "prj_8f31c2", "external_id": "MP-48213", "status": "customer_inquiry", "name": "Srivastava Residence, Indira Nagar", "owner": "Kavya Rao", "design_profile": "Residential IN", "location": { "latitude": 26.8826, "longitude": 80.9995, "source": "geocoded" }, "created_at": "2026-10-24T14:02:11+05:30" } Same external_id again, same body → 200 OK, same project. Same external_id, different body → 409 external_id_conflict.
HTTP/1.1 202 Accepted { "status": "needs_review", "review_id": "rev_5Hc2", "external_id": "MP-48297", "reasons": [ { "code": "address_not_geocoded", "field": "site" } ] }
| Field | Type | Required | Notes |
|---|---|---|---|
external_id | string, ≤ 64 | Yes | Your lead id. Unique per organisation. Makes retries safe. |
customer.name | string, ≤ 255 | Yes | Becomes customer_name. |
customer.mobile | E.164 string | Yes | +91 assumed if 10 digits. Matched against open projects. |
customer.email | string | No | Becomes customer_email_id. |
site.address_line_1, city, state, pincode | strings | Yes | Geocoded. If it does not resolve to one point, the lead is held (202). |
site.latitude, site.longitude | decimal | No | If sent, used instead of geocoding. |
sanctioned_load_kw | number, 0–1000 | No | Above 100 kW the lead is held for review. |
monthly_bill_inr | number | No | Fills average monthly bill on Energy. |
partner.id, partner.name | strings | Yes | Shown on the project and echoed in webhooks. |
Use it to catch up if you missed a webhook. Only returns projects your key created. proposal.ready is false until a designer generates one.
GET https://api.surgepv.com/v1/projects/prj_8f31c2 or /v1/projects?external_id=MP-48213 Authorization: Bearer spv_live_7Kq2…9f2c HTTP/1.1 200 OK { "id": "prj_8f31c2", "external_id": "MP-48213", "status": "proposal_shared", "owner": "Kavya Rao", "proposal": { "ready": true, "system_kwp": 4.64, "url": "https://app.surgepv.com/share/p/…", "url_expires_at": "2026-11-24T10:41:30+05:30" } }
Set one endpoint per organisation in Settings › Integrations › Webhooks. Only projects your key created are sent. Answer 2xx within 10 seconds; anything else is retried after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h, then marked failed. The same event keeps the same id on every retry, so drop ones you have seen.
| Event | When |
|---|---|
proposal.ready | A designer generates a proposal. Carries system size and a 30-day share link. |
project.status_changed | Status moves, with from and to (customer_inquiry, engineering_design, proposal_shared … lost). |
POST {your endpoint} X-SurgePV-Event: proposal.ready X-SurgePV-Delivery: dlv_01JB8R4C2N X-SurgePV-Signature: t=1792905090,v1=5d1c…77aa { "id": "evt_01JB8Q2K7M", "type": "proposal.ready", "created_at": "2026-10-25T10:41:30+05:30", "data": { "project_id": "prj_8f31c2", "external_id": "MP-48213", "partner_id": "LUM-UP-0417", "status": "proposal_shared", "system_kwp": 4.64, "proposal_url": "https://app.surgepv.com/share/p/…", "proposal_url_expires_at": "2026-11-24T10:41:30+05:30" } }
# Python: check the signature before trusting the body t, v1 = parse(header) # t=..., v1=... expected = hmac_sha256(secret, f"{t}.{raw_body}") ok = compare_digest(expected, v1) and abs(now() - int(t)) < 300
Signature: HMAC-SHA256 with your signing secret over t + "." + raw body. Reject if older than 5 minutes.
Errors are JSON: {"error": "<code>", "message": "…", "fields": {…}}.
| HTTP | error | Meaning |
|---|---|---|
| 400 | validation_failed | A required field is missing or malformed. Body lists each field. |
| 401 | invalid_key | Key missing, wrong, expired or revoked. |
| 403 | missing_scope | Key lacks the scope, e.g. proposals:read for GET. |
| 404 | not_found | No such project in your organisation. Another organisation's id answers the same way. |
| 409 | external_id_conflict | external_id already used with a different body. |
| 413 | payload_too_large | Body over 64 KB. |
| 429 | rate_limited | Over the limit. Wait for Retry-After seconds. |
| 5xx | server_error | Safe to retry with the same Idempotency-Key. |
1.0 · 25 Oct 2026. First version: POST /v1/projects, GET /v1/projects/{id}, webhooks proposal.ready and project.status_changed.