v1 REST · OpenAPI 3.1
Документація API
Документація — англійською (in English), як прийнято для API.
https://solispec.com/v1Projects, versions, reports, proposals and reviews of your Solispec organization. Authenticate with an organization API key (cabinet → API): Authorization: Bearer sol_…. Read keys can only read; write keys act as a member and never above their creator’s role. Requests without cookies only (a request carrying cookies is treated as a browser session).
Authentication
- Create a key in the cabinet → API (owner or admin, Business and White Label). It is shown once; we keep only its hash.
readkeys read;writekeys act as a member, never above the role of the person who made the key. A key stops working when it is revoked or its creator leaves the organization.- Send requests without cookies: a request with cookies is treated as a browser session.
- Rate limit
- per key: Business 60, White Label 300 (429 + Retry-After)
- Monthly quota
- per organization: Business 10 000 included; more are counted and billed, never refused
curl https://solispec.com/v1/projects \
-H "Authorization: Bearer sol_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"Errors
Errors are JSON { error, reason?, limit?, plan? }:
| Status | Meaning | Example body |
|---|---|---|
| 400 | Bad request | {"error":"bad_request"} |
| 401 | No key, an unknown or revoked key, or the key creator left the organization | {"error":"unauthorized"} |
| 402 | The plan does not include this (or the API) | {"error":"plan_limit","limit":"api","plan":"pro"} |
| 403 | forbidden: the key scope or the creator role does not allow it; key_not_allowed: keys cannot call this route | {"error":"key_not_allowed"} |
| 404 | Not found in this organization | {"error":"not_found"} |
| 429 | Too many requests for this key (see Retry-After) | {"error":"rate_limited"} |
| 428 | the calculation disclaimer must be accepted once in the cabinet | {"error":"disclaimer_required"} |
Endpoints
read works with any key · write needs a write key
Account
- GET
/v1/meThe organization and the key in useread
Projects
- GET
/v1/projectsList projects (filters: status, clientId, tag, q; cursor paging)read - POST
/v1/projectsCreate a project (optionally with a snapshot)write - GET
/v1/projects/{id}A project with its draft and versionsread - PATCH
/v1/projects/{id}Rename, set status, client or tagswrite - PUT
/v1/projects/{id}/draftSave the draft snapshot (optimistic rev)write
Versions
- POST
/v1/projects/{id}/versionsFreeze the draft as a version, recalculated on the server (calc_id); body { label? }write - GET
/v1/projects/{id}/versions/{n}A saved version with its snapshot and summaryread - POST
/v1/projects/{id}/versions/{n}/restoreRestore a version into the draftwrite
Reports
- GET
/v1/projects/{id}/versions/{n}/reportThe calculation report as HTML (query: country UA|PL|EU, locale uk|en|pl)read - POST
/v1/projects/{id}/versions/{n}/pdfQueue the report as PDF (body { country?, locale? }); 200 when it is already ready; poll the documentread - GET
/v1/documents/{id}A generated document: queued | ready | failedread - GET
/v1/documents/{id}/fileDownload a ready PDFread
Offers
- POST
/v1/projects/{id}/versions/{n}/offersCreate a commercial proposal (КП) from a versionwrite - GET
/v1/offersList proposals (projectId)read - GET
/v1/offers/{id}A proposal with its status, views and acceptanceread - PATCH
/v1/offers/{id}Edit a draft proposal (validDays, terms, clientId)write - POST
/v1/offers/{id}/sendMark as sent: freezes the figures, returns the client linkwrite - POST
/v1/offers/{id}/revokeWithdraw the client linkwrite - POST
/v1/offers/{id}/pdfQueue the proposal as PDFwrite
Reviews
- POST
/v1/projects/{id}/versions/{n}/reviewSend a version to engineer review (Business)write - GET
/v1/reviewsEngineer reviews (status, projectId)read
Clients
- GET
/v1/clientsList clients (q, cursor)read - POST
/v1/clientsCreate a clientwrite - GET
/v1/clients/{id}A client with their projectsread - PATCH
/v1/clients/{id}Update a clientwrite - DELETE
/v1/clients/{id}Delete a clientwrite
Webhooks
Add an https endpoint in the cabinet → API → Webhooks and choose events. Each event is a POST of { id, type, createdAt, data } with these headers:
Solispec-EventSolispec-DeliverySolispec-TimestampSolispec-Signature: v1=<hex HMAC-SHA256(secret, timestamp + "." + body)>
non-2xx or timeout (10 s): after 1 m, 5 m, 30 m, 2 h, 6 h; failed after 6 attempts. Deliveries can repeat — use the delivery id to stay idempotent.
Events
| Event | data |
|---|---|
version.created | {"projectId":"uuid","n":"integer","calcId":"string|null"} |
document.ready | {"documentId":"uuid","kind":"report|offer","projectId":"uuid","versionN":"integer"} |
offer.viewed | {"offerId":"uuid","projectId":"uuid"} |
offer.accepted | {"offerId":"uuid","projectId":"uuid","acceptedName":"string","acceptedAt":"date-time","variant":"{ key: grid|hybrid|gen, title: string, total: number|null } | null"} |
review.signed | {"reviewId":"uuid","projectId":"uuid","versionN":"integer","signedOn":"date"} |
ping | {"message":"string"} |
Verify every delivery before trusting it
const { createHmac, timingSafeEqual } = require('node:crypto');
// headers: the request headers (lower-case names); body: the raw request body string
function verifySolispec(secret, headers, body, nowSec = Math.floor(Date.now() / 1000)) {
const ts = String(headers['solispec-timestamp'] || '');
const sig = Buffer.from(String(headers['solispec-signature'] || '').replace(/^v1=/, ''));
if (!/^\d+$/.test(ts) || Math.abs(nowSec - Number(ts)) > 300) return false; // older than 5 minutes
// the key is the whole whsec_… string as UTF-8 bytes (not base64-decoded)
const want = Buffer.from(createHmac('sha256', secret).update(ts + '.' + body).digest('hex'));
return sig.length === want.length && timingSafeEqual(sig, want);
}Examples
curl https://solispec.com/v1/projects \
-H "Authorization: Bearer sol_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"const r = await fetch('https://solispec.com/v1/projects', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.SOLISPEC_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Roof, 12 kWp' }),
});
if (r.status === 429) await new Promise((ok) => setTimeout(ok, Number(r.headers.get('Retry-After')) * 1000));