e-bon
e-bon.ro
API reference

Billing

REST endpoints for managing the organization's Stripe subscription — checkout sessions, customer portal, subscription status, cancel/resume, invoices, and the Stripe webhook handler.

Billing

The Billing API wraps Stripe so the Portal (and any other authenticated client) can start a checkout, open the customer portal, read subscription status, cancel or resume a subscription, list invoices, and — on the Stripe side — receive Stripe webhook events that drive the organization's plan state. All routes live under /api/v1/billing.

Billing routes are not API-key authenticated. Every endpoint except the inbound Stripe webhook sits behind the Portal JWT middleware (jwtAuth); mutating endpoints additionally require the Owner or Admin role. There is no API-key scope that grants access to billing — log in via POST /api/v1/auth/login and use the returned access token as Authorization: Bearer <jwt>. See Authentication › Use JWT authentication.

The error envelope, rate limits and pagination conventions are documented once on API overview; only the per-endpoint error codes are listed in full on this page.

POST /api/v1/billing/create-subscription

Creates a Stripe Checkout Session for the authenticated user's organization. If the organization already has a Stripe customer (stripeCustomerId), the existing customer is reused; otherwise a new one is created. Returns the Stripe sessionId, optional hosted url and optional clientSecret for embedded checkout.

  • Auth: Portal JWT, role Owner or Admin.

Request body

FieldTypeRequiredNotes
priceIdstringnoStripe price ID. Defaults to the configured Pro price (stripe.proPriceId); fails if neither is supplied.
successUrlstringyesAbsolute URL Stripe redirects to on successful checkout.
cancelUrlstringyesAbsolute URL Stripe redirects to if the user backs out.

Response (200 OK)

{
  "sessionId": "cs_test_a1b2c3",
  "url": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3",
  "clientSecret": null
}

Example

curl -X POST https://api.e-bon.ro/api/v1/billing/create-subscription \
  -H "Authorization: Bearer <portal-jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "successUrl": "https://app.e-bon.ro/billing/success",
    "cancelUrl": "https://app.e-bon.ro/billing/cancel"
  }'

Error codes

  • VALIDATION_ERROR (400) — body failed Zod validation (missing/invalid URLs, empty priceId).
  • BAD_REQUEST (400) — no priceId provided and no default Pro price configured on the server.
  • UNAUTHORIZED (401) — missing or invalid JWT.
  • FORBIDDEN (403) — caller does not have Owner or Admin role.
  • NOT_FOUND (404) — organization document missing.

The full HTTP catalogue is on API overview › HTTP error code catalogue.

POST /api/v1/billing/portal

Creates a Stripe Customer Portal session so the user can manage their own subscription, update payment methods and view invoices on the Stripe-hosted page.

  • Auth: Portal JWT, role Owner or Admin.

Request body

FieldTypeRequiredNotes
returnUrlstringyesAbsolute URL Stripe redirects back to when the user is done.

Response (200 OK)

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

Example

curl -X POST https://api.e-bon.ro/api/v1/billing/portal \
  -H "Authorization: Bearer <portal-jwt>" \
  -H "Content-Type: application/json" \
  -d '{ "returnUrl": "https://app.e-bon.ro/billing" }'

Error codes

  • VALIDATION_ERROR (400) — body failed Zod validation (missing/invalid returnUrl).
  • BAD_REQUEST (400) — organization has no active subscription (stripeCustomerId is unset).
  • UNAUTHORIZED (401) — missing or invalid JWT.
  • FORBIDDEN (403) — caller does not have Owner or Admin role.
  • NOT_FOUND (404) — organization document missing.

GET /api/v1/billing/subscription

Returns the current subscription state for the authenticated user's organization. If the organization has no Stripe subscription yet, the response is { "plan": "free", "status": null, "subscription": null } — the route still returns 200 OK in that case.

  • Auth: Portal JWT (any authenticated org member).

Response (200 OK)

{
  "plan": "pro",
  "status": "active",
  "subscription": {
    "id": "sub_abc123",
    "customerId": "cus_abc123",
    "priceId": "price_pro",
    "currentPeriodEnd": "2026-05-09T00:00:00.000Z",
    "cancelAtPeriodEnd": false,
    "status": "active"
  }
}

Example

curl https://api.e-bon.ro/api/v1/billing/subscription \
  -H "Authorization: Bearer <portal-jwt>"

Error codes

  • UNAUTHORIZED (401) — missing or invalid JWT.
  • NOT_FOUND (404) — organization document missing.

POST /api/v1/billing/cancel

Schedules the organization's subscription for cancellation at the end of the current billing period. The plan stays usable until currentPeriodEnd.

  • Auth: Portal JWT, role Owner or Admin.

Response (200 OK)

{
  "subscription": {
    "id": "sub_abc123",
    "status": "active",
    "cancelAtPeriodEnd": true,
    "currentPeriodEnd": "2026-05-09T00:00:00.000Z"
  }
}

Example

curl -X POST https://api.e-bon.ro/api/v1/billing/cancel \
  -H "Authorization: Bearer <portal-jwt>"

Error codes

  • BAD_REQUEST (400) — no active subscription to cancel.
  • UNAUTHORIZED (401) — missing or invalid JWT.
  • FORBIDDEN (403) — caller does not have Owner or Admin role.
  • NOT_FOUND (404) — organization document missing.

POST /api/v1/billing/resume

Resumes a subscription that was previously scheduled for cancellation. Clears cancelAtPeriodEnd.

  • Auth: Portal JWT, role Owner or Admin.

Response (200 OK)

{
  "subscription": {
    "id": "sub_abc123",
    "status": "active",
    "cancelAtPeriodEnd": false,
    "currentPeriodEnd": "2026-05-09T00:00:00.000Z"
  }
}

Example

curl -X POST https://api.e-bon.ro/api/v1/billing/resume \
  -H "Authorization: Bearer <portal-jwt>"

Error codes

  • BAD_REQUEST (400) — no active subscription to resume.
  • UNAUTHORIZED (401) — missing or invalid JWT.
  • FORBIDDEN (403) — caller does not have Owner or Admin role.
  • NOT_FOUND (404) — organization document missing.

GET /api/v1/billing/invoices

Returns a paginated list of Stripe invoices for the organization. Pagination uses Stripe's startingAfter cursor (the previous page's last invoice ID), not the cursor convention used by Receipts. If the organization has no Stripe customer yet, the response is { "invoices": [], "hasMore": false } with 200 OK.

  • Auth: Portal JWT (any authenticated org member).

Query parameters

ParameterTypeDefaultNotes
limitinteger10Page size, 1100.
startingAfterstringStripe invoice ID to use as cursor. Returns invoices created after this one.

Response (200 OK)

{
  "invoices": [
    {
      "id": "in_abc123",
      "number": "ACME-0001",
      "date": 1712649600,
      "amountDue": 4900,
      "currency": "ron",
      "status": "paid",
      "pdfUrl": "https://files.stripe.com/.../invoice.pdf",
      "hostedInvoiceUrl": "https://invoice.stripe.com/i/acct_…"
    }
  ],
  "hasMore": true
}

Example

curl "https://api.e-bon.ro/api/v1/billing/invoices?limit=20" \
  -H "Authorization: Bearer <portal-jwt>"

Then to fetch the next page:

curl "https://api.e-bon.ro/api/v1/billing/invoices?limit=20&startingAfter=in_abc123" \
  -H "Authorization: Bearer <portal-jwt>"

Error codes

  • VALIDATION_ERROR (400) — bad query (limit > 100).
  • UNAUTHORIZED (401) — missing or invalid JWT.
  • NOT_FOUND (404) — organization document missing.

POST /api/v1/billing/webhook

Inbound Stripe webhook handler. Stripe calls this endpoint directly — do not invoke it yourself. The route is unauthenticated for the regular auth middleware, but it requires:

  • A Stripe-Signature header.
  • The raw request body (the route relies on raw-body parsing — JSON-parsed input is rejected).

The signature is verified against the configured Stripe webhook secret; on success the event updates the organization's plan, stripeCustomerId, stripeSubscriptionId and related fields.

Response (200 OK)

{ "received": true }

Error codes

  • BAD_REQUEST (400) — missing Stripe-Signature header, request body not received as a Buffer, or signature verification failed against the configured webhook secret.
You generally do not need to test this endpoint with curl — use the Stripe CLI (stripe listen --forward-to) when developing locally, and configure the production endpoint URL inside your Stripe Dashboard.

See also

  • Authentication — JWT login flow used by every billing endpoint above.
  • API overview — base URL, error envelope, rate limits, idempotency, pagination.
  • Webhooks — outbound webhooks from e-bon to your own URL (separate system from this Stripe webhook).