v1 REST · OpenAPI 3.1

Documentación de la API

La documentación está en inglés, como es habitual para las API.

Base URLhttps://solispec.com/v1

Projects, 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.
  • read keys read; write keys 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
shellAuthenticated request
curl https://solispec.com/v1/projects \
  -H "Authorization: Bearer sol_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

Errors

Errors are JSON { error, reason?, limit?, plan? }:

StatusMeaningExample body
400Bad request{"error":"bad_request"}
401No key, an unknown or revoked key, or the key creator left the organization{"error":"unauthorized"}
402The plan does not include this (or the API){"error":"plan_limit","limit":"api","plan":"pro"}
403forbidden: the key scope or the creator role does not allow it; key_not_allowed: keys cannot call this route{"error":"key_not_allowed"}
404Not found in this organization{"error":"not_found"}
429Too many requests for this key (see Retry-After){"error":"rate_limited"}
428the 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-Event
  • Solispec-Delivery
  • Solispec-Timestamp
  • Solispec-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

Eventdata
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

node.jsverifySolispec()
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

shellList projects
curl https://solispec.com/v1/projects \
  -H "Authorization: Bearer sol_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
javascriptCreate a project, honour Retry-After
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));