Stigg Contracts — Entitlements and Invoicing
A contract is the source of truth for a sales-led deal: what the customer can do, and what you charge for it. It groups subscriptions for one customer, and those subscriptions grant the entitlements — the contract is the agreement they belong to, not a second entitlement mechanism. It can also carry a billing contract, which produces invoices. The halves are independent — a contract with no billing contract still provisions access.
The contract itself holds no commercial terms. No price, quantity or commitment fields — only a name, PO number, activation window, and its subscription links. Negotiated price and committed volumes live on the custom subscription (
priceOverrides,charges,unitQuantity,minimumSpend—stigg-subscriptions). Priced line items exist only on the billing half, below.
Before You Start
Per the umbrella stigg skill: search first. Confirm shapes and field names against the docs.
No SDK surface. Contracts are not in
@stigg/node-server-sdkor@stigg/js-client-sdk. Use REST (canonical here,https://api.stigg.io/api/v1, headerX-API-KEY) or GraphQL.
Availability. Not enabled for every account. If the endpoints 404, it isn't turned on — ask Stigg rather than working around it.
How would you like to start?
The dashboard asks exactly this, with two independent checkboxes — Provision access and Set up billing — so there are three ways to start. Establish which first; it decides every step below.
| Choice | What the contract does | Send |
|---|---|---|
| Provisioning only | Groups custom subscriptions and grants their entitlements. No invoices. | setupBilling: false + subscriptions |
| Billing only | Priced line items and invoices, no subscriptions attached. | setupBilling: true, no subscriptions |
| Both | Entitlements and invoicing on one agreement — the usual shape for a signed order form. | setupBilling: true + subscriptions |
setupBilling defaults to true, so provisioning-only must be asked for explicitly.
Vocabulary — recognize every name, say only one. UI: "Provision access" / "Set up billing". Docs: "Entitlement provisioning" / "Billing contract" / "Both". API:
setupBilling.Never show the user a field name —
setupBillingincluded. Say "provisioning only", "billing only" or "both". That holds for every field below: the user confirms the deal in the words of their order form, not in JSON.
The Operation Map
| Op | Purpose | Reference |
|---|---|---|
| Create, attach, detach, amend, archive, read | The contract and its membership | inline below |
| Set up billing | Terms, settings, priced line items, totals, publish | below + references/billing-and-invoices.md |
| Pick a pricing model | Deal shape → model + rows, and the shapes with no model | references/pricing-models.md |
| Invoices | List, filter, one invoice, the PDF, mark paid | references/billing-and-invoices.md |
| Credit notes | Settle a change once a contract can no longer be edited | references/billing-and-invoices.md |
Provision Access
Group custom subscriptions and grant their entitlements. No invoices unless billing is also set up.
Prerequisites
Only custom plans qualify — custom plans and their add-ons, not self-service ones. Every subscription
must be custom-priced: a CUSTOM plan, or a PAID plan provisioned with paymentCollectionMethod: "NONE". A FREE-plan subscription can never join (stigg-pricing-modeling).
Provision the customer's account first — create the contract only once the customer exists.
Three server-side rules: one subscription per product, one currency, and every subscription belongs to the contract's customer.
Both: the order to do it in
From an order form, create the contract first and let it provision the subscriptions — provisioning
standalone leaves one live and unattached until a second write links it. Both contract-first shapes are one
transaction: POST /contracts with newSubscription entries, or the same entries on
POST /contracts/:id/subscriptions later.
So: elicit → create the contract with its subscriptions → verify entitlements → price the lines.
Create the Contract
setupBilling: false is the difference — and it defaults to true, so send it explicitly.
curl -X POST "https://api.stigg.io/api/v1/contracts" \
-H "X-API-KEY: <YOUR_API_KEY>" -H "content-type: application/json" \
-d '{
"customerId": "customer-acme",
"contractId": "contract-acme-po-4821",
"name": "Acme 2026",
"poNumber": "PO-4821",
"setupBilling": false,
"subscriptions": [ { "existingSubscriptionId": "subscription-acme-platform" } ]
}'
Each entry is exactly one of existingSubscriptionId or newSubscription — the full
provision-subscription body with its own customerId and planId (stigg-subscriptions). Only
customerId is required; omit subscriptions for an empty contract. The create is atomic — a failure
leaves nothing behind.
Always send your own contractId — the idempotency key, derived from the deal (the PO or order
number). The same one again returns the existing contract; without it a retry silently duplicates, and
contracts cannot be deleted.
Attach and Detach Subscriptions
C=https://api.stigg.io/api/v1/contracts/contract-acme-2026
# Attach existing ones; `subscriptions` with create's own entries provisions new ones instead
curl -X POST "$C/subscriptions" -H "X-API-KEY: <KEY>" -H "content-type: application/json" \
-d '{ "subscriptionIds": ["subscription-acme-analytics"] }'
# Detach — unlink only; the subscription stays active
curl -X DELETE "$C/subscriptions/subscription-acme-analytics" -H "X-API-KEY: <KEY>"
Idempotent: re-attaching, or detaching what isn't attached, changes nothing. A subscription on a different contract is rejected, not moved.
Don't use
PATCH /contracts/:idto add a subscription. ItssubscriptionIdsreplaces the whole set, unlinking any ID you leave out. Use it only to declare complete membership.
Amend, Archive, Read
PATCH /contracts/:id (name, PO number, activation dates), GET /contracts/:id,
GET /customers/:customerId/contracts, GET /contracts?state=ACTIVE, POST /contracts/:id/archive.
Archive is not detach. Archiving cancels the contract and every subscription on it — the customer loses access. To remove a subscription while keeping it running, detach it.
What Provisioning Alone Does NOT Give You
No billing contract: billingId and nextInvoice stay null, no invoices are generated, and
cancel-billing rejects the contract — intended, not a misconfiguration. Adding billing later is one-way
(references/billing-and-invoices.md).
Contract State
Stored state is DRAFT until canceled; the state you read back is derived — ACTIVE once any
subscription on it is active. So a provisioning-only contract flips to ACTIVE on its own, with no publish
step. Field table, state machine, error codes, contract.* events: references/contract-model.md.
Set Up Billing
Negotiated terms, priced line items, invoices. Needs a billing contract — created with setupBilling: true, or enabled later on a provisioning-only one (one-way; billing is never removed).
Full walkthrough: references/billing-and-invoices.md. Five things first:
- Confirm the whole reading before the first write. An order form is not a spec. Read back every line
item with its pricing model and rows, the terms, the legal entity, the customer — and get
agreement before any write call — the pre-publish review is too late, the lines already exist.
Surface contradictions rather than resolving them. Order forms lose lines predictably — usage-based lines show no amount,
Includedlines belong at price 0, a per-period table is one contract, "purchase order reference" is oftenN/Aand is not the order number. The reference covers each. - Line items are priced by existing pricing models — never a table you build.
GET /contracts/pricing-modelslists them withisPayAsYouGoandeditableInputs; supply only the inputs a model exposes, named asrowsaccepts them. Charge type is in advance (IN_ADVANCE) or in arrears (IN_ARREARS) — the engine'sDURING_USE/AFTER_USEare rejected. Pay-as-you-go applies only to metered items, takes no quantity (usage supplies it), and is always in arrears. Resolve a model byisPayAsYouGo+ shape, never by name — five of the ten share a name with their opposite-category twin, and both accept the same rows and return 200.references/pricing-models.mdmaps deal shapes to models, with the nearest wrong neighbour for each. - A legal entity needs a payment account.
GET /contracts/legal-entitieslists each entity's accounts; an emptypaymentAccountsis the common half-configured state and no API creates one — raise it before building, or the contract looks complete then refuses to publish. Set both, plus period, currency and payment terms, withPATCH .../billing/settingsand.../billing/terms— contract-wide, so apply them before any per-item exception. - Two checks before publishing, both real on a draft.
GET .../billing/summaryagainst the order form's figures, andGET .../billing/invoice-schedule(?count=N) against its billing schedule. The totals can be exactly right while the invoices are wrong — items agreeing on money but differing on when they bill split into extra invoices, and only the schedule shows it. Publishing refuses without a period, payment terms and a legal entity. - A live contract is amended, not rewritten.
POST .../billing/edit-activechanges it in place — it stays active and only affected invoices regenerate. Needs a body —{}if nothing to choose. The line-item and terms endpoints instead reset a live contract to a draft, dropping unsent invoices. Rejections areBillingContractOperationRejected, with the remedy in the message; read it rather than retrying.BillingContractEditBlockedis terminal: credit note or new contract.
Invoices and credit notes: same reference.
When NOT to Use This Skill
- Self-serve catalog modeling — plans, addons, charges, coupons →
stigg-pricing-modeling. - Subscription lifecycle with no contract attached →
stigg-subscriptions. - Credit currencies, grants, ledger →
stigg-credits. - Per-entity budgets and usage limits →
stigg-governance.
Common Mistakes
Each mistake — the fix:
- Omitting
setupBillingfor provisioning only — defaults totrue; sendfalse. - Attaching a self-serve subscription — self-serve plans can't join. Use a custom plan.
PATCH subscriptionIdsto add one — it replaces the whole set. UsePOST /contracts/:id/subscriptions.- Archiving to remove a subscription — it cancels the contract and every subscription. Detach instead.
- Treating the contract as what grants entitlements — the subscriptions do. An empty contract grants nothing and holds no price or quantity.
- Retrying a 409 on product duplication — a modeling conflict, not transient. Detach that product's subscription first.
- Inventing a pricing table — pricing comes from an existing model, chosen by id. Fill only the inputs it exposes.
- Publishing without reading the summary — afterwards those figures are what the customer is invoiced.
- Retrying
BillingContractEditBlocked— terminal: a credit note or a new contract. - Expecting a URL for an invoice PDF — inline base64, no durable link.
GET .../pdfwaits, or poll the generate/status pair.