Skip to main content

Plan Billing

Three calls let an application show a merchant where they stand against this month's invoice allowance and sell them the upgrade, without sending them to the GoRoute dashboard. A fourth, GET /api/v1/billing/summary, answers the same question for a customer who is on an agreed package rather than a plan.

Available on production since 8 September 2026

These calls are on both the production and the test service.

They reached production in the release of 8 September 2026, and they were confirmed on production on 10 September 2026 by calling each address with no credential: all four answered 401 asking for an API key, while an address that does not exist on the same host answered 404. Nothing invents an authentication challenge, so the routes are there.

An earlier version of this page, written on 7 September 2026, said these calls were on the test service only and would fail on production. That was true when it was written and is not true now. If you postponed the work on the strength of it, you can build against production today.

All paths below are relative to the production base URL https://app.goroute.ai/peppol-api. The test service serves the same paths under its own base URL โ€” see Environments โ€” and nothing else differs between the two.

The calls on this pageโ€‹

Method and pathWhat it does
GET /api/v1/billing/usageWhere the organisation stands against this month's invoice allowance, whether it buys on a price list or on agreed terms, the state of its subscription, and whether checkout and the billing portal are open to it
POST /api/v1/billing/checkout-sessionStarts a hosted Stripe checkout for a plan โ€” or changes the plan in place when a subscription already exists
POST /api/v1/billing/portal-sessionOpens Stripe's customer portal, where a merchant changes the card, downloads receipts and cancels
GET /api/v1/billing/summaryOne organisation's billing position for the current period, whether it is on a white-label package or on a GoRoute plan. Needs the billing:manage permission โ€” see its own section

Every one of them is answered for the organisation the credential belongs to. A request with no credential is refused with 401 and MISSING_API_KEY.

The first three carry no separate billing permission: any API key or signed-in session of the organisation may call them, because the numbers they return are the organisation's own counters. GET /api/v1/billing/summary is the exception and says so in its own section.

Plans managed by a store appโ€‹

A merchant who installed the GoRoute app from the Shopify App Store or the Wix App Market pays through that store, not Stripe. The app records the outcome with PUT /api/v1/billing/plan (API key with the integrate scope; body plan, source, external_ref, trial_ends_at, current_period_end, quota_override) and hands the plan back on uninstall with DELETE /api/v1/billing/plan.

source accepts shopify or wix. Anything else is refused.

Both calls answer with the shape of GET /api/v1/billing/usage, which carries billing_source (stripe, shopify or wix), billing_external_ref, trial_ends_at, current_period_end and quota_override; allowance is always the effective monthly quota. Write your client to accept all three values โ€” an organisation whose plan came from the Wix app returns wix, and code that only knows stripe and shopify will reject a live answer.

While the source is shopify or wix, the checkout and portal calls answer 409 with error plan_managed_externally, and Stripe webhooks leave the organisation's plan alone. The refusal names where the merchant changes the plan instead: the GoRoute app in the Shopify admin for shopify, and the GoRoute app in your Wix dashboard for wix.

The monthly invoice allowanceโ€‹

Every plan includes a number of outbound invoices a month. Free is the only plan that stops at it. On every paid plan the allowance is a number on the bill: sending carries on past it and the overage is billed. A white-label client on an agreed package is never stopped either โ€” the package is the contract.

Usage is counted as outbound documents in the calendar month, whatever became of them afterwards. A document that failed to deliver still counts.

Where the stop is appliedโ€‹

Five calls refuse to issue a document once a Free organisation has used its allowance for the month:

Method and path
POST /api/v1/invoices
POST /api/v1/invoices/import/csv
POST /api/v1/pos/fuel-invoice
POST /api/v1/pos/exchange-invoice
POST /api/v1/pos/sale

The refusal is HTTP 402 with error_code PLAN_LIMIT_REACHED:

{
"error_code": "PLAN_LIMIT_REACHED",
"message": "The Free plan includes 50 invoices a month and 50 have been issued in 2026-09. Upgrade to keep issuing, or the allowance renews on 2026-10-01.",
"plan": "free",
"allowance": 50,
"used": 50,
"period": "2026-09",
"resets_on": "2026-10-01",
"request_id": "3f6c1b9a-2f4e-4b8d-9c31-7a2d5e8f0b14"
}
FieldMeaning
planThe plan key the organisation is on
allowanceInvoices the plan includes in a month
usedInvoices already issued in this period
periodThe calendar month being counted, as YYYY-MM
resets_onThe date the allowance renews, as YYYY-MM-DD โ€” the first of the following month

Those five fields are there so an application can tell a merchant exactly what happened and when it clears, rather than showing them a bare error. Read error_code, not the message text.

GET /api/v1/billing/usageโ€‹

curl -X GET https://app.goroute.ai/peppol-api/api/v1/billing/usage \
-H "X-API-Key: pk_live_your_key_here"

Response โ€” an organisation on the Free plan, 23 invoices into the month:

{
"plan": "free",
"plan_name": "Free",
"period": "2026-09",
"resets_on": "2026-10-01",
"allowance": 50,
"used": 23,
"remaining": 27,
"hard_limit": true,
"exhausted": false,
"self_serve": true,
"account_manager": null,
"brand_name": null,
"subscription": {
"status": "none",
"current_period_end": null,
"cancel_at_period_end": false
},
"checkout_available": true,
"portal_available": false,
"plans": [ /* one entry per plan on sale โ€” see "The plan catalogue" below */ ]
}

The /* โ€ฆ */ line is an abbreviation for this page. The API returns a normal JSON array there.

Fieldsโ€‹

FieldTypeMeaning
planstringThe organisation's plan key
plan_namestringHow that plan is presented. It is not always a tidied-up version of the key โ€” the professional key is presented as Growth โ€” so show plan_name and match on plan
periodstringThe calendar month being counted, YYYY-MM
resets_onstringDate the allowance renews, YYYY-MM-DD
allowanceintegerInvoices the plan includes in a month
usedintegerInvoices issued so far in period
remaininginteger or nullallowance minus used, never below zero. null when the plan carries no monthly allowance figure at all
hard_limitbooleantrue only when this organisation will actually be stopped at the allowance โ€” the Free plan, and not on a white-label package
exhaustedbooleantrue when hard_limit is set and the allowance is used up. This is the flag that predicts a 402
self_servebooleantrue when the organisation buys from the price list. false for enterprise, sandbox, demo and white-label package customers, who are on agreed terms
account_managerstring or nullWhere to write to change agreed terms. Always null when self_serve is true. It can also be null when the party those terms are with keeps no address on record
brand_namestring or nullWhose terms they are. Also null when self_serve is true
subscriptionobjectThe last thing the payment provider told us โ€” see below
checkout_availablebooleantrue when a plan can actually be bought on this environment right now. It is false when the environment has no payment configuration, and false for anyone not on the price list
portal_availablebooleantrue when this organisation has a customer record with the payment provider, so the portal can be opened
plansarrayThe plan catalogue โ€” empty for an organisation on agreed terms

The subscription objectโ€‹

FieldTypeMeaning
statusstringnone when there has never been a subscription, otherwise the provider's own status
current_period_endstring or nullWhen the paid period runs out, as an ISO 8601 timestamp
cancel_at_period_endbooleantrue when the merchant has cancelled and is running out the period they paid for

Three statuses keep the organisation on its paid plan: active, trialing and past_due. past_due is deliberate โ€” the provider is still chasing the payment, and the plan stays until it gives up. When the status becomes canceled, unpaid or incomplete_expired the organisation goes back to the Free plan, with the Free allowance and the Free hard stop.

The plan catalogueโ€‹

Each entry of plans has these keys:

FieldTypeMeaning
planstringThe plan key, which is what you send to POST /api/v1/billing/checkout-session
namestringThe plan's display name
usd_monthintegerThe list price, in US dollars a month
blurbstringOne line describing what the plan includes
monthly_invoicesintegerThe plan's monthly invoice allowance
purchasablebooleantrue when this plan can be bought on the environment you are calling

This page does not restate the prices. They are in usd_month, they are on goroute.ai, and a copy of them here would go stale without anyone noticing. Read the catalogue and render it; do not hard-code it.

POST /api/v1/billing/checkout-sessionโ€‹

curl -X POST https://app.goroute.ai/peppol-api/api/v1/billing/checkout-session \
-H "X-API-Key: pk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"plan": "starter", "return_url": "https://your-store.example/wp-admin/admin.php?page=goroute"}'

Request bodyโ€‹

FieldRequiredTypeMeaning
planyesstringThe plan key to buy
return_urlyesstringA page on your own site where the merchant lands afterwards

Which plan keys are accepted is decided by the environment, not by this page. Send a key that appears in the plans array from GET /api/v1/billing/usage with purchasable set to true. The Free plan cannot be bought. Anything else is refused with 400 and UNKNOWN_PLAN, and the message lists the keys that environment will accept.

return_url must be an https address on a host you control, with no #fragment, and no longer than 1,500 characters. Plain http is accepted only for localhost and 127.0.0.1, so that a developer can work locally.

Two possible answersโ€‹

A merchant with no subscription gets a hosted checkout page to be sent to:

{
"url": "https://checkout.stripe.com/c/pay/..."
}

Send them there. When they come back, your return_url will carry two extra query parameters: goroute_plan, with the plan key, and session_id, with the provider's checkout session identifier. If they abandon the checkout they come back to your return_url with neither.

A merchant who already has a live subscription does not get a checkout page at all. The existing subscription is switched to the new plan, the payment provider works out the proration, and the answer says so:

{
"changed": true,
"plan": "professional",
"url": "https://your-store.example/wp-admin/admin.php?page=goroute&goroute_plan=professional"
}

There is nothing to pay at that moment. Treat changed as the signal to show a confirmation rather than to redirect. The new plan takes effect immediately, so the allowance a merchant sees changes as soon as they are back.

Refusalsโ€‹

Statuserror_codeWhen
400UNKNOWN_PLANThe plan key is not on sale on this environment, or is the Free plan
400BAD_RETURN_URLreturn_url is not an https address on a real host, carries a fragment, or is too long
409ALREADY_ON_PLANThe organisation's live subscription is already on the plan you asked for
503BILLING_NOT_CONFIGUREDSelf-serve upgrades are not open on this environment. checkout_available on the usage call tells you this in advance
502STRIPE_ERRORThe payment provider could not be reached, or refused. Safe to retry

POST /api/v1/billing/portal-sessionโ€‹

curl -X POST https://app.goroute.ai/peppol-api/api/v1/billing/portal-session \
-H "X-API-Key: pk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"return_url": "https://your-store.example/wp-admin/admin.php?page=goroute"}'

return_url is optional here and follows the same rules as on checkout. Leave it out and the merchant is returned to their GoRoute company settings page instead of to your own.

Response:

{
"url": "https://billing.stripe.com/p/session/..."
}

Send the merchant to that address. Everything after that โ€” card details, receipts, cancelling โ€” happens on the payment provider's pages, not on ours.

Refusalsโ€‹

Statuserror_codeWhen
404NO_SUBSCRIPTIONThis organisation has never paid for a plan, so there is no customer record to open a portal on. Check portal_available first
400BAD_RETURN_URLThe return_url you supplied breaks one of the rules above
503BILLING_NOT_CONFIGUREDBilling is not configured on this environment
502STRIPE_ERRORThe payment provider could not be reached, or refused. Safe to retry

GET /api/v1/billing/summaryโ€‹

The three calls above answer "how much of my plan have I used, and what can I buy". This one answers a different question: what is this organisation's billing position for the current period, whether it buys a GoRoute plan or sits under a white-label provider on an agreed package. It is the call to build a billing overview screen on.

curl -X GET https://app.goroute.ai/peppol-api/api/v1/billing/summary \
-H "X-API-Key: pk_live_your_key_here"

Who may call itโ€‹

Unlike the three calls above, this one requires the billing:manage permission.

CredentialAllowed
Signed-in user with the org_owner or finance roleYes
Signed-in user with org_admin, operations, developer or viewerNo โ€” org_admin is deliberately excluded from billing
API key with the admin (or legacy all) scopeYes
API key with the read, write, send or integrate scopeNo

Anyone else is refused with 403 and error_code INSUFFICIENT_PERMISSION. A request with no credential is refused with 401 and MISSING_API_KEY.

This permission check is always enforced. Some permission requirements elsewhere on the platform are still being phased in, so a call can succeed today that will be refused later; this one is not among them. Build against the table above and it will not change under you.

Three shapes, chosen by what the organisation isโ€‹

Every answer carries a mode field. Read mode first and branch on it โ€” package and plan are never both filled in, and the one that does not apply is null.

modeThe organisation isWhat it gets
packageA white-label client with a package assigned by its providerThe package terms, and this billing cycle's usage and bill
unassignedA white-label client with no package assigned yetUsage only. Deliberately no plan table โ€” GoRoute's tiers and quotas are not this customer's commercial terms, and are not ours to show to somebody else's customer
planA direct GoRoute customerA curated set of the plan's limits, and usage against the included allowance

Fields present in every mode:

FieldTypeMeaning
modestringpackage, unassigned or plan, as above
periodstringThe calendar month being counted, YYYY-MM
usageobjectWhat was issued and what is billable. Its contents differ by mode โ€” see below
packageobject or nullFilled in only when mode is package
planobject or nullFilled in only when mode is plan
billing_contactobjectWhere this organisation's own invoices are sent: billing_email and contact_name
billed_byobject or nullWho issues the invoice: name and support_email of the white-label provider. null for a direct GoRoute customer, who is billed by GoRoute

billed_by never carries the provider's own accounts-payable address. It is the provider's customer-facing support contact, so "your provider issues the final invoice" is something a customer can act on.

Usage counts outbound documents that were submitted, whatever became of them afterwards โ€” the same rule as the allowance further up this page.

mode: "package"โ€‹

{
"mode": "package",
"period": "2026-09",
"package": {
"package_id": "pkg_starter_om",
"name": "Starter (Oman)",
"currency": "OMR",
"monthly_fee": 25.0,
"included_invoices": 200,
"overage_per_invoice": 0.05,
"setup_fee": 100.0,
"billing_frequency": "quarterly",
"assigned_at": "2026-07-01T00:00:00Z"
},
"usage": {
"period": "2026-09",
"billing_frequency": "quarterly",
"cycle_label": "Q3 2026",
"cycle_months": 3,
"monthly_fee": 25.0,
"cycle_fee": 75.0,
"included_invoices": 600,
"invoice_count": 742,
"billable_invoices": 142,
"overage_amount": 7.1,
"currency": "OMR",
"total": 82.1
},
"plan": null,
"billing_contact": { "billing_email": "accounts@client.example", "contact_name": "A. Al Balushi" },
"billed_by": { "name": "Muscat Digital", "support_email": "support@muscatdigital.example" }
}

The trap in this shape: package.included_invoices is the allowance per month, while usage.included_invoices is the allowance over the whole billing cycle. On a quarterly package the second is three times the first, and the same goes for package.monthly_fee against usage.cycle_fee. Bill and compare against the usage figures; show the package figures as the terms. Reading the monthly number as the cycle allowance is how you under-count an overage.

The package object repeats the terms as they were snapshotted when the package was assigned:

FieldTypeMeaning
package_idstringThe provider's package identifier
namestringThe package's name as the provider set it
currencystringThe currency of every money figure in this answer. Defaults to OMR when the package does not name one โ€” do not assume euros
monthly_feenumberThe monthly rate
included_invoicesintegerInvoices included per month
overage_per_invoicenumberCharged per invoice past the cycle allowance
setup_feenumberA one-off, shown for reference; it is not part of usage.total
billing_frequencystringmonthly, quarterly or yearly
assigned_atstring or nullWhen the provider assigned the package

And the usage object for this mode:

FieldTypeMeaning
periodstringThe calendar month, YYYY-MM โ€” the same machine key used everywhere else on this page
billing_frequencystringThe cycle this bill covers
cycle_labelstringThe cycle for a human to read: Sep 2026 monthly, Q3 2026 quarterly, 2026 yearly. Show this, match on period
cycle_monthsintegerMonths in the cycle: 1, 3 or 12
monthly_feenumberThe monthly rate, repeated
cycle_feenumbermonthly_fee multiplied by cycle_months
included_invoicesintegerThe allowance over the whole cycle
invoice_countintegerInvoices submitted over the whole cycle
billable_invoicesintegerinvoice_count minus included_invoices, never below zero
overage_amountnumberbillable_invoices at overage_per_invoice
currencystringAs above
totalnumbercycle_fee plus overage_amount. The setup fee is not in it

mode: "unassigned"โ€‹

A white-label client whose provider has not yet assigned a package. There is nothing to bill against, so the three usage figures other than the count are null rather than zero โ€” zero would read as "an allowance of none".

{
"mode": "unassigned",
"period": "2026-09",
"package": null,
"usage": { "invoice_count": 34, "included_invoices": null, "billable_invoices": null },
"plan": null,
"billing_contact": { "billing_email": "accounts@client.example", "contact_name": "A. Al Balushi" },
"billed_by": { "name": "Muscat Digital", "support_email": "support@muscatdigital.example" }
}

Show the count and point the customer at billed_by. Do not fill the gap with GoRoute plan prices: this customer has no GoRoute plan and never sees one.

mode: "plan"โ€‹

A direct GoRoute customer. billed_by is always null here โ€” reaching this shape at all means no white-label provider was found above the organisation.

{
"mode": "plan",
"period": "2026-09",
"package": null,
"usage": { "invoice_count": 412, "included_invoices": 1000, "billable_invoices": 0 },
"plan": {
"billing_plan": "professional",
"billing_source": "stripe",
"billing_external_ref": null,
"limits": {
"monthly_invoices": 1000,
"monthly_transactions": 1000,
"max_participants": 25,
"webhook_enabled": true,
"scheduled_sending": true,
"bulk_operations": true,
"approval_workflow": false,
"support_level": "email",
"sla_hours": 24
}
},
"billing_contact": { "billing_email": "accounts@seller.example", "contact_name": "J. Peeters" },
"billed_by": null
}
FieldTypeMeaning
plan.billing_planstringThe plan key. It is the key, not the display name โ€” professional is presented as Growth
plan.billing_sourcestringstripe, shopify when the plan is managed by the Shopify app, or wix when it is managed by the Wix app. Defaults to stripe
plan.billing_external_refstring or nullThe subscription reference at that source, when the plan is managed externally
plan.limitsobjectNine fields, listed below
usage.invoice_countintegerInvoices submitted in period
usage.included_invoicesintegerThe effective monthly allowance โ€” a per-organisation quota override replaces the plan's own figure, so this can differ from the plan tier's published number
usage.billable_invoicesintegerThe count past the allowance, never below zero, and 0 when there is no allowance figure at all

plan.limits carries these nine fields and no others. It is a deliberately curated subset: rate limits and internal batch settings are not exposed here.

FieldTypeMeaning
monthly_invoicesintegerThe effective monthly invoice allowance, the same figure as usage.included_invoices
monthly_transactionsintegerTransactions included a month
max_participantsintegerPeppol participants the organisation may register
webhook_enabledbooleanWhether webhooks are included
scheduled_sendingbooleanWhether sending can be scheduled
bulk_operationsbooleanWhether bulk operations are included
approval_workflowbooleantrue on the Business, Agency and Enterprise plans, false on the others. It does not switch anything on. No part of the platform reads this flag, and no invoice is ever held for approval on any plan โ€” see Approval Events.
support_levelstringcommunity, email, priority or dedicated
sla_hoursinteger or nullResponse-time commitment in hours. null when the plan carries none

Choosing between this call and GET /api/v1/billing/usageโ€‹

They are not interchangeable.

  • GET /api/v1/billing/usage is for a plan strip: what is left this month, and the upgrade. Any credential of the organisation may read it, so a store plugin can show it.
  • GET /api/v1/billing/summary is for a billing overview: the terms, the cycle and the bill, including for customers who have no GoRoute plan at all. It needs billing:manage, so do not put it behind a screen every member of staff can open.

Building a plan stripโ€‹

The three calls are meant to be used together, and this is the order that works:

  1. Call GET /api/v1/billing/usage when your settings page loads. Show used of allowance, and the date in resets_on.
  2. If hard_limit is true, warn as used approaches allowance โ€” this merchant will be stopped, not billed for the overage.
  3. Show an upgrade button only when checkout_available is true, and build its choices from the plans array rather than from a list you maintain.
  4. Show a "manage billing" link only when portal_available is true.
  5. When self_serve is false, show no plan cards at all. Show brand_name and account_manager instead, so the merchant writes to the right people about terms that are already agreed.
  6. Handle 402 with PLAN_LIMIT_REACHED wherever you issue a document. It carries everything you need to explain the stop.
  • Error Codes โ€” the platform's error code reference
  • Authentication โ€” API keys, permissions and rate limits
  • Environments โ€” telling the test and production services apart