BasegridAPI reference

Basegrid API

Everything in the model, available to your tools.

The same endpoints the Basegrid app and the SketchUp extension use. Base URL https://app.basegrid.com.au.

Authentication schemes
AuthHow it works
Bearer tokenSend Authorization: Bearer <token> where the token is either a bgc_ connection credential from /app/connections, or a SketchUp OAuth access token. Scoped to the token's organisation.
SessionRequires a signed-in browser session (the Supabase auth cookies set by /api/auth/login). Scoped to the caller's organisation by row-level security.
Session · owner or adminRequires a signed-in session whose role in the organisation is owner or admin. Members receive 403.
PublicNo credentials required.
Stripe signatureCalled by Stripe only; verified with the webhook signing secret.

Conventions

All endpoints live under /api. Request and response bodies are JSON unless stated. Every error response has the same shape — { "error": "…" } — with a conventional status: 400 validation, 401 not signed in, 403 not allowed, 404 not yours or not found, 409 conflict, 5xx server. Money is stored in integer cents and quantities as decimals. All timestamps are ISO 8601 in UTC. Tenant isolation is enforced by Postgres row-level security, so a request can never read or write another organisation's rows.

Connection API v1

Read-only endpoints for connected tools such as the SketchUp extension. Authenticate with a bearer token. Organisation owners and admins create and revoke bgc_ credentials at /app/connections; a credential is shown once and only its SHA-256 digest is stored, so a lost credential is replaced, not recovered. The SketchUp extension may instead present an OAuth 2.1 access token (Authorization Code + PKCE S256) issued to the registered public client; the token's client_id claim is verified before the user's first organisation membership sets tenant scope. Every 200 carries a strong ETag; send it back as If-None-Match to receive 304 Not Modified with no body. Revocation takes effect on the next request.

GET/api/v1/oauth/configPublic

OAuth discovery for the SketchUp client

Public endpoints and the registered client ID a native client needs to start an Authorization Code + PKCE flow. enabled is false when OAuth is not configured on the server.

Responses

  • 200Discovery document. Never cached.
    {
      "enabled": true,
      "authorization_endpoint": "https://<project>.supabase.co/auth/v1/oauth/authorize",
      "token_endpoint": "https://<project>.supabase.co/auth/v1/oauth/token",
      "client_id": "…",
      "redirect_uri": "http://127.0.0.1:43821/oauth/callback",
      "scope": "openid email profile"
    }
GET/api/v1/material-libraryBearer token

The organisation's material library

Material types with their materials, dimensions and appearance, plus the organisation's tool configuration: takeoff_groups (every group, with status active or retired so saved models can still resolve retired ones) and tools (the global SketchUp tool catalogue, each with the ids of the groups it may offer in this organisation — configured at /app/tools; nothing is allowed by default and new groups are never linked automatically). A tool runs with one of its groups selected and every item it creates inherits that group. generated_roles is retained for older extensions and reports each role's tool groups. Every id in tools[].takeoff_group_ids and generated_roles[].takeoff_group_ids is present in takeoff_groups; arrays are [] rather than null. The ETag changes when a tool, group, assignment or material changes. Texture objects include one-hour signed_url values for privately stored images; solid colours have signed_url: null and expose color and render_spec instead. When a material has no construction texture, construction_texture falls back to its type's default_texture. ETags roll before signed URLs expire so long-running clients periodically receive fresh URLs.

Headers

FieldTypeNotes
If-None-MatchstringThe ETag from a previous 200. Returns 304 when unchanged.

Responses

  • 200The library, with ETag.
    {
      "api_version": "v1",
      "organisation": { "id": "uuid", "name": "Example Builders" },
      "takeoff_groups": [
        { "id": "uuid", "name": "Concrete slabs", "status": "active" }
      ],
      "tools": [
        { "id": "concrete.slab_from_face", "name": "Concrete Slab", "status": "active",
          "takeoff_group_ids": ["uuid"] },
        { "id": "structure.beam", "name": "Beam", "status": "active", "takeoff_group_ids": [] },
        { "id": "timber.wall_frame", "name": "Timber Wall Framing", "status": "active",
          "takeoff_group_ids": [] }
      ],
      "generated_roles": [
        {
          "id": "concrete.slab_from_face.slab_body",
          "tool_id": "concrete.slab_from_face",
          "takeoff_group_ids": ["uuid"]
        }
      ],
      "material_types": [
        {
          "id": "uuid",
          "name": "LVL",
          "profile": "rectangular",
          "uom": "m",
          "density_kg_m3": 550,
          "notes": null,
          "status": "active",
          "updated_at": "2026-08-20T01:02:03.000Z",
          "default_texture": null,
          "materials": [
            {
              "id": "uuid",
              "name": "240x45 LVL",
              "profile": "rectangular",
              "grade": "E13",
              "finish": null,
              "dimensions_mm": { "width": 45, "depth": 240, "thickness": null, "height": null,
                                 "diameter": null, "wall_thickness": null, "flange_width": null,
                                 "web_thickness": null, "flange_thickness": null, "girth": null, "folds": null },
              "mass_per_uom": null,
              "status": "active",
              "updated_at": "2026-08-20T01:02:03.000Z",
              "construction_texture": {
                "texture_id": "uuid", "name": "Pine grain", "kind": "labelled",
                "signed_url": "https://…", "color": null, "render_spec": null,
                "aspect_ratio": 1, "natural_width_mm": null, "natural_height_mm": null,
                "fit": "fitted", "width_mm": 45, "height_mm": 45, "rotation": 0
              },
              "display_texture": null
            }
          ]
        }
      ]
    }
  • 304Unchanged; reuse the cached body.
  • 401Token missing, malformed, unknown, or revoked.
  • 500Library or texture signing could not be completed.

Example

curl https://app.basegrid.com.au/api/v1/material-library \
  -H "Authorization: Bearer bgc_…"
GET/api/v1/takeoff-groupsBearer token

The organisation's takeoff groups

The workspace-wide list of headings a takeoff is organised under, in the estimator's order. Use abbreviation for compact item codes and column headings.

Headers

FieldTypeNotes
If-None-MatchstringThe ETag from a previous 200. Returns 304 when unchanged.

Responses

  • 200The groups, with ETag.
    {
      "api_version": "v1",
      "organisation": { "id": "uuid", "name": "Example Builders" },
      "takeoff_groups": [
        {
          "id": "uuid",
          "name": "Wall Framing",
          "abbreviation": "WF",
          "description": "Studs, plates, noggins and lintels.",
          "sort_index": 3,
          "updated_at": "2026-08-27T04:05:06.000Z"
        }
      ]
    }
  • 304Unchanged; reuse the cached body.
  • 401Token missing, malformed, unknown, or revoked.
  • 500The groups could not be loaded.

Example

curl https://app.basegrid.com.au/api/v1/takeoff-groups \
  -H "Authorization: Bearer bgc_…"

Authentication

Session endpoints for the web app. A successful sign-in sets Supabase auth cookies on the response; browsers send them automatically to every other session-authenticated endpoint. Sign-up creates the user, their organisation and an owner membership in one step.

POST/api/auth/signupPublic

Create an account and organisation

Request body · application/json

FieldTypeNotes
emailrequiredstringLower-cased and trimmed.
passwordrequiredstringAt least 8 characters.
fullNamerequiredstringUp to 120 characters.
orgNamestringWorkspace name. Defaults to fullName.

Responses

  • 201Signed in immediately.
    { "redirectTo": "/app" }
  • 202Email confirmation required before the session exists.
    { "notice": "Check you@example.com — we've sent a link to confirm your account." }
  • 400Validation failed or the email is taken.
    { "error": "Human-readable message." }
POST/api/auth/loginPublic

Sign in with email and password

Request body · application/json

FieldTypeNotes
emailrequiredstring
passwordrequiredstring
nextstringSame-origin path to return to. Anything else falls back to /app.

Responses

  • 200Cookies set.
    { "redirectTo": "/app" }
  • 401Those details didn't match an account.
    { "error": "Human-readable message." }

Example

curl https://app.basegrid.com.au/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"…"}' -c cookies.txt
POST/api/auth/logoutPublic

Sign out

Responses

  • 200Session cleared.
    { "redirectTo": "/login" }

Takeoff groups

An organisation-level library of headings a takeoff is organised under — Framing, External Cladding, Roofing. Names are unique per organisation (case-insensitive); abbreviations are optional, 1–8 uppercase letters or digits, and unique where set. Managed at /app/takeoff-groups.

GET/api/takeoff-groupsSession

List takeoff groups

Responses

  • 200Ordered by sort_index, then name.
    {
      "takeoffGroups": [
        { "id": "uuid", "org_id": "uuid", "name": "Wall Framing", "abbreviation": "WF",
          "description": null, "sort_index": 3,
          "created_at": "2026-08-27T…", "updated_at": "2026-08-27T…" }
      ]
    }
POST/api/takeoff-groupsSession

Create a takeoff group

New groups are appended to the end of the order.

Request body · application/json

FieldTypeNotes
namerequiredstring1–120 characters.
abbreviationstring | nullUp to 8 letters or digits; upper-cased on save.
descriptionstring | nullUp to 2000 characters.

Responses

  • 201Created.
    { "takeoffGroup": { … } }
  • 409Name or abbreviation already used.
    { "error": "Human-readable message." }

Example

curl https://app.basegrid.com.au/api/takeoff-groups -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"name":"Wall Framing","abbreviation":"WF"}'
PATCH/api/takeoff-groups/{id}Session

Update a takeoff group

Send only the fields to change. At least one is required.

Request body · application/json

FieldTypeNotes
namestring
abbreviationstring | nullNull clears it.
descriptionstring | nullNull clears it.
sort_indexintegerPosition in the list, from 0.
statusactive | retiredRetired groups stay resolvable but cannot receive new tool assignments.

Responses

  • 200Updated.
    { "takeoffGroup": { … } }
  • 404Not found in your organisation.
    { "error": "Human-readable message." }
  • 409Name or abbreviation already used.
    { "error": "Human-readable message." }
DELETE/api/takeoff-groups/{id}Session

Delete a takeoff group

Responses

  • 200Deleted (or already gone).
    { "deleted": true }

SketchUp tools

SketchUp tools are global product definitions with stable ids (concrete.slab_from_face, structure.beam, timber.wall_frame). A tool runs with one takeoff group selected and every item it creates inherits that group. Each organisation chooses which of its groups a tool may offer — several can be made available, empty by default, managed at /app/tools or from a takeoff group's form. New groups are never linked automatically. The extension reads the result from GET /api/v1/material-library.

GET/api/sketchup-toolsSession

The tool catalogue with this organisation's available groups

Responses

  • 200Every tool with its assigned group ids. roles is the compatibility view for older extensions and mirrors the tool's groups.
    {
      "tools": [
        {
          "id": "concrete.slab_from_face",
          "name": "Concrete Slab",
          "status": "active",
          "description": "…",
          "takeoff_group_ids": ["uuid"],
          "roles": [
            { "id": "concrete.slab_from_face.slab_body", "name": "Slab body", "unit": "m3",
              "takeoff_group_ids": ["uuid"] }
          ]
        },
        { "id": "structure.beam", "name": "Beam", "status": "active", "description": "…",
          "takeoff_group_ids": [], "roles": [] }
      ]
    }
PUT/api/sketchup-tools/{toolId}/takeoff-groupsSession · owner or admin

Replace the groups a tool may offer

Sets the complete list for the caller's organisation. Groups must belong to your organisation and be active; a retired group may be kept if it is already assigned but cannot be newly added. Changes the material-library ETag.

Request body · application/json

FieldTypeNotes
takeoff_group_idsrequireduuid[]The complete list of available groups.

Responses

  • 200Saved.
    { "tool_id": "concrete.slab_from_face", "takeoff_group_ids": ["uuid"] }
  • 400Unknown group, or a retired group newly added.
    { "error": "Human-readable message." }
  • 404Unknown tool.
    { "error": "Human-readable message." }

Example

curl -X PUT https://app.basegrid.com.au/api/sketchup-tools/concrete.slab_from_face/takeoff-groups   -b cookies.txt -H "Content-Type: application/json"   -d '{"takeoff_group_ids":["<uuid>"]}'
PUT/api/sketchup-tools/roles/{roleId}/takeoff-groupsSession · owner or admin

Deprecated: replace a role's groups

Retained for the extension's compatibility window. Configuration is now per tool, so this writes to the role's tool — identical to PUT /api/sketchup-tools/{toolId}/takeoff-groups — and will be removed once the extension migration is confirmed.

Request body · application/json

FieldTypeNotes
takeoff_group_idsrequireduuid[]The complete list.

Responses

  • 200Saved.
    { "role_id": "concrete.slab_from_face.slab_body", "tool_id": "concrete.slab_from_face", "takeoff_group_ids": ["uuid"] }
  • 400Unknown group, or a retired group newly added.
    { "error": "Human-readable message." }
  • 404Unknown role.
    { "error": "Human-readable message." }

Materials

A material type owns the shape (profile) and unit; each material under it carries the values — dimensions in millimetres, grade and finish — plus a construction texture and an optional display texture. Profiles: rectangular, sheet, round, i_section, rhs, shs, chs, flashing, bar_group, mesh, bulk. Units: m, m2, m3, each. Which dimensions a profile requires is documented in docs/MATERIAL_MODEL.md.

POST/api/material-typesSession

Create a material type

Request body · application/json

FieldTypeNotes
namerequiredstring1–120 characters, unique per organisation.
profilerequiredenumOne of the profiles above.
uomrequiredenumm, m2, m3 or each.
density_kg_m3number | nullDerives mass for every size under the type.
notesstringUp to 2000 characters.

Responses

  • 201Created.
    { "materialType": { … }, "redirectTo": "/app/materials/<id>" }
  • 409A type with that name exists.
    { "error": "Human-readable message." }
PATCH/api/material-types/{id}Session

Rename a material type

Request body · application/json

FieldTypeNotes
namerequiredstring1–120 characters.

Responses

  • 200Renamed.
    { "materialType": { … } }
  • 404Not found in your organisation.
    { "error": "Human-readable message." }
  • 409A type with that name exists.
    { "error": "Human-readable message." }
DELETE/api/material-types/{id}Session

Delete a material type and its materials

Responses

  • 200Deleted.
    { "deleted": true, "redirectTo": "/app/materials" }
POST/api/material-types/{id}/materialsSession

Add a material to a type

Multipart form. Dimension fields are accepted only if the type's profile uses them, and the profile's required dimensions must be present. Appearance fields describe the construction texture; repeat them with a display_ prefix for the optional display texture. Images must be JPEG, PNG or WebP and 256 KB or smaller.

Request body · multipart/form-data

FieldTypeNotes
namerequiredstringMaterial name, 1–200 characters.
gradestringMGP10, F17, 300PLUS…
finishstringZincalume, Colorbond…
width_mm, depth_mm, thickness_mm, height_mm, diameter_mm, wall_thickness_mm, flange_width_mm, web_thickness_mm, flange_thickness_mm, girth_mm, folds, bars, spacing_mmnumberPositive. Which apply depends on the profile.
mass_per_uomnumberOverrides the density-derived mass.
appearance_modeimage | colorDefaults to image.
texturefileRequired when appearance_mode=image.
texture_aspect_rationumberWidth ÷ height of the image.
texture_width_mmnumberReal-world width the image represents.
texture_height_mmnumberReal-world height.
texture_size_lastwidth | heightWhich dimension was edited last.
texture_colorstring#rrggbb, when appearance_mode=color.
display_*as aboveSame fields for the display texture.

Responses

  • 201Created.
    { "material": { … } }
  • 400Validation or image problem.
    { "error": "Human-readable message." }
  • 404That material type no longer exists.
    { "error": "Human-readable message." }
PATCH/api/materials/{id}Session

Update a material

Send only the fields to change. Use it to rename, set grade/finish, retire, or assign textures: texture_id (construction) and display_texture_id (display) must be textures in your organisation; null clears them. fit is fitted (stretch to the face) or natural (tile at the texture's physical size); when natural, send the size pair.

Request body · application/json

FieldTypeNotes
namestring1–200 characters.
gradestring | null
finishstring | null
mass_per_uomnumber | nullOverrides the density-derived mass.
statusactive | retired
texture_iduuid | nullConstruction texture.
texture_fitfitted | natural
texture_width_mm, texture_height_mmnumber | nullSend together.
texture_rotation0 | 90
display_texture_iduuid | nullDisplay texture.
display_texture_fitfitted | natural
display_texture_width_mm, display_texture_height_mmnumber | nullSend together.
display_texture_rotation0 | 90

Responses

  • 200Updated.
    { "material": { … } }
  • 404Material or texture not found in your organisation.
    { "error": "Human-readable message." }

Example

curl -X PATCH https://app.basegrid.com.au/api/materials/<id> -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"texture_id":"<texture uuid>","texture_fit":"natural","texture_width_mm":200,"texture_height_mm":200}'
DELETE/api/materials/{id}Session

Delete a material

Query parameters

FieldTypeNotes
materialTypeIduuidOptional; refreshes that type's page in the app.

Responses

  • 200Deleted.
    { "deleted": true }

Textures

A texture is the appearance a material carries into the model. Three kinds: solid — a flat colour; labelled — text rendered server-side to a 512 px PNG (Arimo, Arial-metric) at the physical size you give it, over either a flat colour or an uploaded picture (source_image_url), in which case the colour acts as a tint; image — an uploaded photo with no text, created through the material form. Solid and labelled textures are fully editable: every formatting field lives in format and the PNG is re-rendered on change, so the picture under a label is kept untouched in storage. Assign a texture to a material with PATCH /api/materials/{id}. Sizes are in millimetres and describe what the swatch represents in the real world, so a natural-fit material tiles it correctly.

GET/api/texturesSession

List textures

Query parameters

FieldTypeNotes
kindsolid | labelled | imageOptional filter.

Responses

  • 200Ordered by name.
    {
      "textures": [
        {
          "id": "uuid", "name": "40MPa", "kind": "labelled",
          "image_url": "material-textures/<org>/<file>.png", "source_image_url": null, "color": null,
          "aspect_ratio": 1, "width_mm": 200, "height_mm": 200,
          "format": {
            "background": "#b8b8b8", "background_opacity": 1,
            "text": "40MPa", "text_color": "#101614",
            "font_family": "sans", "font_size_percent": 18, "font_weight": 400,
            "text_align": "center", "vertical_align": "middle",
            "line_height": 1.05, "padding_percent": 8
          },
          "created_at": "…", "updated_at": "…"
        }
      ]
    }
POST/api/texturesSession

Create a solid or labelled texture

Formatting fields are optional and default to the values shown in the list example (centred, 18 % of height, weight 400, ink #101614, 8 % padding). Use \n in text for multiple lines. Labelled textures need text, width_mm and height_mm; the image is rendered 512 px on its long side and must come out under 256 KB.

Request body · application/json

FieldTypeNotes
kindrequiredsolid | labelled
namestringDefaults to the text (labelled) or Colour #RRGGBB (solid).
backgroundrequiredstring#rrggbb. The fill, or the tint over an image.
background_opacitynumber0–1. Default 1, or 0 when an image is present.
textstring | nullRequired for labelled. Up to 200 characters.
text_colorstring#rrggbb. Default #101614.
font_familysans | monoDefault sans (Arimo, Arial-metric).
font_size_percentnumber4–60, % of image height. Default 18. Shrinks to fit.
font_weight400 | 600 | 700Default 400.
text_alignleft | center | rightDefault center.
vertical_aligntop | middle | bottomDefault middle.
line_heightnumber0.8–2. Default 1.05.
padding_percentnumber0–40. Default 8.
width_mm, height_mmnumberPhysical size. Both or neither; required for labelled.

Responses

  • 201Created.
    { "texture": { … } }
  • 400Validation failed.
    { "error": "Human-readable message." }
  • 500Rendering or upload failed.
    { "error": "Human-readable message." }

Example

curl https://app.basegrid.com.au/api/textures -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"kind":"labelled","text":"40MPa","background":"#b8b8b8","width_mm":200,"height_mm":200}'

# Text over a picture
curl https://app.basegrid.com.au/api/textures -b cookies.txt   -F kind=labelled -F text="Exposed Aggregate" -F background=#000000 -F background_opacity=0.25   -F width_mm=600 -F height_mm=600 -F image=@exposed-agg.jpg
GET/api/textures/{id}Session

Get a texture

Includes one-hour signed_url (the rendered texture) and source_signed_url (the picture under a label); null where there is no stored file.

Responses

  • 200The texture.
    { "texture": { …, "signed_url": "https://…", "source_signed_url": null } }
  • 404Not found in your organisation.
    { "error": "Human-readable message." }
PATCH/api/textures/{id}Session

Edit a texture's formatting, size, picture or name

Any subset of the create fields except kind, as JSON or multipart. Solid and labelled textures are re-rendered (a label over a picture is re-composited from the stored source) and superseded files removed; materials using the texture pick up the change. Uploaded image-kind textures accept only name.

Request body · application/json

FieldTypeNotes
name, background, text, …as for createAny formatting or size field.
imagefileMultipart only. Replaces the picture under a labelled texture.
remove_imagebooleanDrop the picture and render on the flat colour instead.

Responses

  • 200Updated.
    { "texture": { … } }
  • 404Not found in your organisation.
    { "error": "Human-readable message." }
  • 409Formatting sent for an uploaded image.
    { "error": "Human-readable message." }

Example

curl -X PATCH https://app.basegrid.com.au/api/textures/<id> -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"text":"40MPa\nN40","font_weight":600,"background":"#b0b0ab"}'
DELETE/api/textures/{id}Session

Delete a texture

Materials and types that used it fall back to no texture.

Responses

  • 200Deleted.
    { "deleted": true }

Projects and takeoff items

A project holds one quote. Takeoff items belong to a project; rate is sent in dollars and stored as rate_cents, and total_cents is computed by the database.

POST/api/projectsSession

Create a project

Request body · application/json

FieldTypeNotes
coderequiredstringYour job code, up to 24 characters, unique per organisation.
namerequiredstringUp to 200 characters.
addressstringUp to 300 characters.

Responses

  • 201Created.
    { "project": { "id": "uuid", "code": "029BRO", "name": "…", "address": "…" }, "redirectTo": "/app/projects/<id>" }
  • 409Project code already in use.
    { "error": "Human-readable message." }
POST/api/projects/{id}/takeoff-itemsSession

Add a takeoff item

Request body · application/json

FieldTypeNotes
codestringCost code, up to 40 characters.
materialrequiredstringUp to 300 characters.
qtyrequirednumberZero or more.
unitstringUp to 16 characters, e.g. m, m2, each.
raterequirednumberDollars; stored as cents.

Responses

  • 201Created, tagged, source manual.
    { "item": { … } }
DELETE/api/projects/{id}/takeoff-items/{itemId}Session

Remove a takeoff item

Responses

  • 200Deleted.
    { "deleted": true }

Workspace

Organisation settings and invitations. Owners and admins only. An invite carries a token; the person joins by opening /invite/{token}, which either signs them up straight into this workspace or, if they already have an account, adds them to it. Email delivery is not wired up, so the link is sent by hand. Invites expire after 14 days and can only be used once.

PATCH/api/orgSession · owner or admin

Rename the workspace

Request body · application/json

FieldTypeNotes
namerequiredstring1–120 characters.

Responses

  • 200Renamed.
    { "org": { "id": "uuid", "name": "…" } }
POST/api/org/invitesSession · owner or admin

Invite someone

Request body · application/json

FieldTypeNotes
emailrequiredstring
roleowner | admin | memberDefaults to member. Only an owner may invite an owner.

Responses

  • 201Invite created. token is the only delivery mechanism — put it in /invite/{token} and send that.
    { "invite": { "id": "…", "email": "…", "role": "member", "token": "…", "expires_at": "…" } }
  • 409That person already has a pending invite.
    { "error": "Human-readable message." }
DELETE/api/org/invites/{id}Session · owner or admin

Withdraw an invite

Responses

  • 200Deleted.
    { "deleted": true }

Billing

Subscription management through Stripe. Both endpoints return a URL to send the browser to.

POST/api/billing/checkoutSession · owner or admin

Start a subscription checkout

Responses

  • 200Stripe Checkout URL.
    { "url": "https://checkout.stripe.com/…" }
  • 503Stripe is not configured.
    { "error": "Human-readable message." }
POST/api/billing/portalSession · owner or admin

Open the customer portal

Responses

  • 200Stripe portal URL.
    { "url": "https://billing.stripe.com/…" }
  • 409No Stripe customer for this workspace yet.
    { "error": "Human-readable message." }
  • 503Stripe is not configured.
    { "error": "Human-readable message." }
POST/api/stripe/webhookStripe signature

Stripe webhook receiver

Receives subscription lifecycle events and writes billing state to the organisation with the service role. Not for client use.

Responses

  • 200Event handled.