Prove value

Customers renew when they can see what your product did for them. Spondo Billing turns the signals you already send into value: you agree what each signal is worth, and it works out the value delivered to each customer over any period and sets it against what they paid. A value receipt freezes that for a period and gives the customer a page that says, for example, "You paid $203. We delivered $3,823 of value."

ConceptWhat it is
Value metricOne kind of value, such as "Contract review avoided" or "Pages read for you": which signal feeds it, how many units each signal is, and what a unit is worth.
Value modelAll your value metrics as one versioned JSON document. Every change publishes a new version.
Delivered valueThe value of one customer's signals over a period. Each signal is valued when it arrives, with the model version in force at its timestamp.
Value receiptA frozen record of delivered value and what the customer was billed, for one customer and one ended period, which you can publish as a page.

Value figures are decimal amounts in the model's currency, such as 120.5 for $120.50.

Define a value metric

A value metric ties a signal to a value. This one says each contract_reviewed signal saves the customer one first-pass contract review, worth $120:

await spondo.request("POST", "/value-metrics", {
  body: {
    name: "Contract review avoided",
    category: "hve",
    unit: { type: "custom", unit: "contracts" },
    formula: { expression: "1", variables: [] },
    signal: { eventName: "contract_reviewed", label: "Contract reviewed" },
    monetaryConversion: { valuePerUnit: 120, label: "Value per contract" },
  },
});
curl https://billing.getspondo.com/api/v1/value-metrics \
  --request POST \
  --header "Authorization: Bearer $SPONDO_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Contract review avoided",
    "category": "hve",
    "unit": { "type": "custom", "unit": "contracts" },
    "formula": { "expression": "1", "variables": [] },
    "signal": { "eventName": "contract_reviewed", "label": "Contract reviewed" },
    "monetaryConversion": { "valuePerUnit": 120, "label": "Value per contract" }
  }'

The response gives the metric's slug (made from its name unless you send one: contract-review-avoided) and the value model version the change published.

What each part means:

  • formula says how many units one signal is. "1" counts each signal as one unit. The name of a number in the signal's data, such as "pages", takes that number: a document_uploaded signal with { "pages": 12 } is 12 units. Formulas can also calculate, with arithmetic, comparisons and min, max, abs, floor, ceil, round and if: review.minutes / 60 turns minutes into hours. Variables are dotted paths into data, and a missing one counts as 0.
  • unit says what a unit is: custom (a count of something you name in unit), duration (time, with durationUnit set to seconds, minutes, hours or days), or monetary (the units are already money).
  • monetaryConversion.valuePerUnit is what one unit is worth. Leave it out to count units without valuing them.
  • category groups metrics in reports: hve (work a person no longer has to do), time, cost, revenue or risk.

The signal must be one Spondo Billing has seen, or one an active plan prices; otherwise the call fails with 404 SIGNAL_NOT_FOUND. One signal can feed several metrics: a reviewed contract can save a lawyer's time and lower your customer's risk.

Change a metric with PATCH /value-metrics/{slug}, for example to agree a new rate, and archive it with DELETE /value-metrics/{slug}. An archived metric values no signals after it was archived, and leaves your lists, but stays readable by slug.

Versions and concurrent changes

Each create, update and archive publishes a new version of the value model. To make sure you're changing the version you think you are, send expectedActiveVersion: if someone else published a version since, the call fails with 409 VERSION_CONFLICT instead of overwriting their change.

Manage the whole model at once

For several metrics, per-customer rates or a model kept in your own repository, upload the whole document with PUT /value-models. It publishes as the next version and takes effect at once; the previous version is archived. The document holds:

FieldWhat it is
currencyThe ISO 4217 currency of every value.
valueTypesThe value metrics, each with a calculation timeline: the unit and rate in force from each effectiveFrom.
formulasHow many units a signal is, for each value type: an expression, optionally with condition bands (for example, a different result for documents over 50 pages).
segmentTablesLookup tables formulas can use, such as an hourly rate by job title.
signalsWhich signals feed which formula, and which data field each formula variable reads.
overridesCustomer-specific terms: signals that count only for some customers, and rates agreed separately with them.

GET /value-models returns the model in force, GET /value-models/versions lists every version and GET /value-models/versions/{version} returns one as it was published. A version can say when it applies, with effectiveFrom and effectiveTo: signals outside that window are valued by the version that covers them. The API reference has a complete example document.

Re-value past signals

Each signal is valued once, when it arrives, so a new model version applies to signals from then on and past periods keep their value. To value past signals with the current model, for example after correcting a rate, run POST /value-models/backfill, optionally for a window ({ "startDate": "2026-10-01", "endDate": "2026-10-31" }). It re-values every customer's signals in the window, up to 100,000 at a time.

Read delivered value

GET /delivered-value totals one customer's value for a period, from the value stored for each signal they sent:

const value = await spondo.request("GET", "/delivered-value", {
  query: { externalCustomerId: "acme-legal", startDate: "2026-10-01", endDate: "2026-10-31" },
});
console.log(value.totals); // { monetaryTotal: 3823, signalCount: 214 }
curl 'https://billing.getspondo.com/api/v1/delivered-value?externalCustomerId=acme-legal&startDate=2026-10-01&endDate=2026-10-31' \
  --header "Authorization: Bearer $SPONDO_API_KEY"

The response breaks the total down by metric (units, rate and value, with the working) and by signal. A date without a time covers the whole day in UTC, so endDate=2026-10-31 includes all of 31 October.

For charts, GET /delivered-value/summary returns the same value as a time series: set interval (hour, day, week, month or quarter) and groupBy (valueMetric, signal, formulaCondition, or dimension with a dimensionKey naming a field in the signals' data).

Issue a value receipt

A value receipt records delivered value for one customer and one period that has ended, next to what they were billed for it and the return on that spend. Once created, its figures never change, so it's safe to send to a customer.

const receipt = await spondo.request("POST", "/value-receipts", {
  body: { externalCustomerId: "acme-legal", startDate: "2026-10-01", endDate: "2026-10-31" },
});
// { id: "vr_…", status: "ready", publicUrlToken: "…", publicUrl: "https://billing.getspondo.com/#receipt/…" }
curl https://billing.getspondo.com/api/v1/value-receipts \
  --request POST \
  --header "Authorization: Bearer $SPONDO_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "externalCustomerId": "acme-legal", "startDate": "2026-10-01", "endDate": "2026-10-31" }'

To show value from one product, add product: { productId: "documind" }, or pass an orderId to scope it to the product of that order's plan. To show the figures in another currency, add product.currency: they're converted at your account's exchange rate, which the receipt keeps (422 EXCHANGE_RATE_MISSING if there isn't one). A period that hasn't ended yet is refused with 400 VALIDATION_ERROR.

To update the figures after more signals arrive or the model changes, create a new receipt for the period and archive the old one.

Publish it

A new receipt is private. Publish it to make its links work: shareUrl, a page Spondo Billing renders that you can link to or show in your app (add mode=embed to frame it), and publicUrl, the same receipt in the Spondo Billing app:

await spondo.request("POST", `/value-receipts/${receipt.id}/publish`, {
  body: { publishExpiresAt: "2027-01-31T00:00:00Z" }, // or leave out for no expiry
});

Anyone with the link can then see the receipt, without signing in: your company and the customer, the period, "You paid … We delivered … of value", the return on their spend, and how each metric's value was worked out. The link is long and random, and it stays the same if you unpublish and publish again. After publishExpiresAt, or once you call POST /value-receipts/{id}/unpublish, the page says the receipt isn't available.

Private receipts in your product

To show a receipt inside your product without publishing it, keep it private and embed its shareUrl with mode=embed. Turn embedding on for the receipt with PATCH /value-receipts/{id} and { "allowEmbed": true }. A private receipt opens only with a token query parameter: a short-lived JWT your backend signs with your receipt signing secret, which POST /value-receipts/rotate-secret creates (it's shown once). Visitors without a token are sent to your login URL, which you set with PATCH /value-receipts/settings. The API reference lists the token's claims.

Archive receipts you no longer need with POST /value-receipts/{id}/archive: archiving unpublishes the receipt, so its links stop working, and it leaves the list (unless you ask for archived=true) and can't be published again. Unarchiving one fails with 409 VALUE_RECEIPT_PERIOD_COVERED when another live receipt already covers the same customer and period.