Skip to content
opportunora.
  • The loop
  • Product
  • Pricing
  • FAQ
  • Docs
  • Blog
Sign in Start free trial

Documentation · Reference

API and webhooks

The REST API under /api/v1 — authentication, the envelope, every endpoint — and the signed webhooks the app sends when something changes, with a verifier you can copy.

Updated 17 September 2026

All pages
  1. Start here

    1. What it is
    2. Create your workspace
    3. Your first hour
    4. The dashboard
  2. The loop

    1. Find leads
    2. Companies and deals
    3. Generate a website
    4. Edit a website
    5. Publish a website
    6. Sites that need a server
    7. Invoices
    8. Track time
    9. Get paid
  3. Connections

    1. Connect Cloudflare
    2. Connect your own domain
    3. Connect Stripe
    4. Connect PayPal
  4. Your workspace

    1. Settings
    2. AI credits and plans
    3. People and roles
    4. Prompt Studio
  5. Reference

    1. API and webhooks
      1. Authentication
      2. The envelope
      3. Endpoints
      4. Webhooks
      5. The events
      6. Verifying the signature
      7. Queue metrics
    2. Run your own deployment
    3. Troubleshooting

Everything you can do with companies, deals, time and invoices in the app, a script can do over a JSON API. And when something happens — a lead found, a site published, an invoice paid — the app can tell another system about it.

Important

API keys and webhook subscriptions have no screen in the current build. They are created by whoever runs the deployment. Everything below works once you have a key or a subscription.

Authentication

The base URL is the app's own address, followed by /api/v1. Send the key in either header:

curl -s "https://app.example.com/api/v1/invoices?status=Sent" \
  -H "Authorization: Bearer $OPPORTUNORA_API_KEY"
curl -s "https://app.example.com/api/v1/invoices" \
  -H "x-api-key: $OPPORTUNORA_API_KEY"

A key looks like lse_ followed by 64 hexadecimal characters. It belongs to one workspace and carries one of the six roles, which decides what it may change — see People and roles. Every key can read everything in its workspace.

Status Error Cause
401 Missing API key. Provide an Authorization: Bearer <key> header. No key sent.
401 Invalid or revoked API key. Unknown or deactivated key.
403 Permission denied: missing invoices.write The key's role cannot do that.

The envelope

Success:

{ "success": true, "data": { "id": "…", "name": "Bluebird Bakery" } }

Failure:

{ "success": false, "error": "Validation failed", "details": [ … ] }
Status When
200 Read, update or action succeeded.
201 Created.
400 The body is not a JSON object, or it failed validation — details lists the problems.
401, 403 Authentication or permission, as above.
404 No such record in this workspace.
409 A business rule refused it: a locked time entry, a paid invoice, a duplicate.
500 Anything else. Some rule refusals arrive as 500 today; read error.

Lists return every matching record in one array. There is no pagination and no sort parameter.

Endpoints

Resource Endpoints List filters
Companies GET POST /companies · GET PATCH DELETE /companies/<id> stage, search, runId, isActive
Opportunities GET POST /opportunities · GET PATCH DELETE /opportunities/<id>
Projects GET POST /projects · GET PATCH DELETE /projects/<id> status
Time entries GET POST /time-entries · GET PATCH DELETE /time-entries/<id>
Invoices GET POST /invoices · GET PATCH DELETE /invoices/<id> status
Invoice actions POST /invoices/<id>/send · /mark-paid · /cancel · /reopen
Websites GET POST /deliverables · GET DELETE /deliverables/<id> status
Approvals GET POST /approval-items · GET /approval-items/<id>

Things to know about them:

  • PUT is the same as PATCH: a partial update. The id in the path wins over any id in the body.
  • Invoices are always created as drafts. A status in the body is ignored; statuses move through the action endpoints. A number in the body only contributes its prefix — the sequence is always assigned by the app.
  • Delete is for drafts. An invoice with any payment on it cannot be deleted; cancel it.
  • Websites cannot be edited over the API. Their content changes through generation and the editor.
  • Two endpoints exist but do not work in this build: POST /deliverables/<id>/publish and POST /approval-items/<id>/decide. Use Approve & publish in the app.

There is no endpoint for discovery runs, generation, packages, settings, users or payment connections.

A company, for example:

curl -s -X POST "$APP_URL/api/v1/companies" \
  -H "Authorization: Bearer $OPPORTUNORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Bakery Meier","city":"Bern","countryCode":"CH"}'

Webhooks

A webhook subscription is a URL, a secret, and the list of events to send to it. For each matching event the app makes one POST:

POST /your/endpoint
Content-Type: application/json
User-Agent: Opportunora-Webhook/1.0
X-Webhook-Event: invoice.paid
X-Webhook-Id: <the subscription's id>
X-Webhook-Signature: sha256=<hex>

{
  "event": "invoice.paid",
  "timestamp": "2026-09-10T12:34:56.789Z",
  "data": { "id": "…", "invoiceNumber": "INV-1042", "amountPaid": 2400, "currency": "CHF" }
}

Warning

Delivery is one attempt with a ten-second timeout. There are no retries. An endpoint that is down or answers with anything but 2xx misses that event for good, so reconcile against the API for anything that must not be missed.

The events

Event Sent when
company.discovered A company is added by hand, or saved by a discovery run.
opportunity.stage_changed A deal moves stage.
deliverable.generated A website version finishes generating.
deliverable.published A website goes live.
approval.decided An approval is decided.
invoice.sent An invoice is sent. Carries its payUrl.
invoice.paid Money is recorded. A payment from Stripe or PayPal adds a payment object with the provider, method, gross, fee and net.
invoice.canceled An invoice is canceled.
invoice.reopened A refunded or partly refunded invoice is put back up for payment.
invoice.refunded A provider refund takes an invoice to Refunded.

A subscription names events exactly. There are no wildcards.

Verifying the signature

The signature is an HMAC-SHA256 of the raw request body with the subscription's secret. Verify it before you parse the body:

// Node 18+
const crypto = require('node:crypto')

function verify(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(header ?? '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

// Express: take the raw body, not the parsed one.
app.post('/hooks/opportunora', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8')
  if (!verify(raw, req.get('X-Webhook-Signature'), process.env.OPPORTUNORA_WEBHOOK_SECRET)) {
    return res.sendStatus(400)
  }
  const { event, data } = JSON.parse(raw)
  // …
  res.sendStatus(200)
})

The signature covers the body only; there is no timestamp in it and no replay window. X-Webhook-Id identifies the subscription, not the delivery, so use the record's own id in data if you need to de-duplicate.

Queue metrics

For autoscaling workers on a self-hosted deployment, GET /api/queues/depth reports queued, running and runnable work across the whole deployment. It is off unless QUEUE_METRICS_TOKEN is set, and it takes that token, not an API key:

curl -H "Authorization: Bearer $QUEUE_METRICS_TOKEN" "$APP_URL/api/queues/depth"
{
  "discovery":  { "queued": 0, "running": 0, "runnable": 0, "oldestWaitSeconds": 0 },
  "generation": { "queued": 0, "running": 0, "runnable": 0, "oldestWaitSeconds": 0 },
  "publish":    { "queued": 0, "running": 0, "runnable": 0, "oldestWaitSeconds": 0 },
  "totalQueued": 0,
  "totalRunning": 0,
  "totalRunnable": 0,
  "scalingDemand": 0
}

Scale on running plus runnable jobs per queue, divided by the capacity of each worker. scalingDemand is the total running plus runnable work. Raw queue depth includes jobs the workspace plan cannot start yet. See Run your own deployment.

Previous Prompt Studio Next Run your own deployment

Did this leave something out?

Write to info@opportunora.com and say which page you were on. The documentation is part of the product, so a gap in it is a bug.

opportunora.

Find local businesses. Build their website. Publish. Invoice. Repeat.

The page

  • The loop
  • Product
  • Docs
  • Blog

Get started

  • Pricing
  • FAQ
  • Start free trial
  • Sign in

Legal

  • Privacy
  • Terms
© 2026 opportunora

Allow analytics cookies?

Google Analytics shows us how people use this page. Choose Only essentials and it never loads; the page then stores nothing but your choice.