EmberlyEmberly Docs

Billing API

Create checkout/portal sessions and manage subscriptions via the Emberly API.

Cloud only. Billing runs on embrly.ca and these endpoints return 404 on self-hosted instances see Cloud vs. Self-Hosted.

  • Requires a session cookie (authenticated browser session), not an upload token these power the dashboard's upgrade/billing flows and aren't intended for external integrations.
  • Requires Stripe to be configured on the server if it isn't, they return 501 (or 503 for sync) instead of processing anything.

Endpoints

MethodPathDescription
POST/api/payments/checkoutStart a subscription checkout session
GET/api/payments/checkoutSame, via query param (?priceId=)
GET/api/payments/portalRedirect to the Stripe customer portal
POST/api/payments/purchaseStart a one-off purchase (extra storage, custom domain)
GET/api/payments/purchaseSame, via query param
GET/api/payments/promo-codesList active, publicly visible promo codes
POST/api/payments/sync-subscriptionRe-sync your Stripe subscriptions into the database

Start a Subscription Checkout

POST /api/payments/checkout

Body:

{
  "priceId": "price_abc123",
  "successUrl": "https://embrly.ca/dashboard",
  "cancelUrl": "https://embrly.ca/pricing",
  "metadata": { "source": "pricing-page" }
}

successUrl/cancelUrl are optional and default to /dashboard and /pricing. metadata must be a flat object of string-to-string pairs — anything else is silently dropped.

Response (200):

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

Redirect the user to url to complete checkout. Any existing account credit is applied automatically.

Errors: 401 unauthenticated · 501 Stripe not configured · 404 user not found


Open the Billing Portal

GET /api/payments/portal

No body. Responds with an HTTP redirect straight to the Stripe customer portal, where the user can update payment methods, view invoices, or cancel.

Errors: 401 unauthenticated · 501 Stripe not configured


One-Off Purchase

POST /api/payments/purchase

For non-subscription add-ons — extra storage or an extra custom domain slot.

Body:

{
  "priceId": "price_extra_storage_10gb",
  "type": "extra_storage",
  "quantity": 1
}

type is extra_storage or custom_domain (informational — stored in Stripe metadata, doesn't change server behavior). quantity defaults to 1.

Response (200):

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

Errors: 401 unauthenticated · 501 Stripe not configured · 404 user not found


List Promo Codes

GET /api/payments/promo-codes

Public-safe list of currently active promotion codes, for display on the pricing page. No authentication required. Codes with metadata.private = "true" in Stripe are excluded.

Response (200):

[
  {
    "id": "promo_abc123",
    "code": "LAUNCH20",
    "percentOff": 20,
    "amountOff": null,
    "currency": null,
    "maxRedemptions": 100,
    "timesRedeemed": 12,
    "expiresAt": null
  }
]

If Stripe isn't configured or the Stripe API call fails, this returns an empty array rather than an error — promo codes are a display-only enhancement.


Sync Subscription

POST /api/payments/sync-subscription

Re-pulls your active Stripe subscriptions into Emberly's database. Useful if a checkout succeeded but the webhook that should have recorded it was missed or hadn't been configured yet.

Response (200):

{ "synced": true, "activeSubscriptions": 1 }

If you have no linked Stripe customer yet, it responds with { "synced": 0, "message": "No Stripe customer linked" } instead of erroring.

Errors: 401 unauthenticated · 503 Stripe not configured


Webhook

POST /api/payments/webhook receives Stripe webhook events (checkout completion, subscription updates/cancellations, invoice events) and is called by Stripe directly, not by clients — it's listed here for completeness only.

On this page