From 070ffb47d4c8bf558335854d3ab4afc295bef265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Wed, 23 Sep 2026 14:57:49 +0200 Subject: [PATCH 1/2] Document the Partner API implementation journey --- content/api/authentication.mdx | 5 +++- content/api/index.mdx | 2 +- content/api/meta.ts | 1 + content/api/partners/go-live.mdx | 27 +++++++++++++++++ content/api/partners/index.mdx | 50 ++++++++++++++++++++++++++++++++ content/api/partners/meta.ts | 6 ++++ content/api/sdk.mdx | 3 ++ 7 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 content/api/partners/go-live.mdx create mode 100644 content/api/partners/index.mdx create mode 100644 content/api/partners/meta.ts diff --git a/content/api/authentication.mdx b/content/api/authentication.mdx index f4022bf..e99466e 100644 --- a/content/api/authentication.mdx +++ b/content/api/authentication.mdx @@ -5,7 +5,7 @@ sidebar: order: 2 --- -The API takes one header. After this page you know which of the four credential shapes to mint for your case, how to scope it, and why a valid token still gets a 403 on some routes. +The API takes one header. After this page you know which credential to mint for your case, how to scope it, and why a valid token still gets a 403 on some routes. ## The header @@ -22,8 +22,11 @@ The server decides what a token is from its prefix before any verifier runs. | `sfa_` | Platform API key | `POST /v1/api-keys`, or the dashboard | | `sfc_` | Space key for an unclaimed anonymous Space | returned by an anonymous `POST /v1/publish` | | `sfm_` | Space machine credential | `POST /v1/spaces/{spaceId}/machine-credentials` | +| signed partner JWT | Customer credential for one external principal | Your verified customer session and active partner issuer | | no prefix | Signed-in session or OAuth access token | dashboard sign-in, device login, or the OAuth token endpoint | +Partner `JWTs` have no Spacefast prefix. The API recognizes their signed `at+jwt` profile. See [Partner API](/api/partners) for issuer activation and customer scope. + ## API keys An `sfa_` key is 56 characters of `[0-9A-Za-z]` after the prefix. It is the credential for the CLI, CI, and server-to-server calls. diff --git a/content/api/index.mdx b/content/api/index.mdx index 645ca5a..7e7695d 100644 --- a/content/api/index.mdx +++ b/content/api/index.mdx @@ -19,7 +19,7 @@ Every operation lives under `/v1`. The version is in the path, so a future `/v2` ## Authentication -One header, four accepted token shapes: +One header for API keys, Space credentials, sessions, OAuth tokens, and partner customer `JWTs`: diff --git a/content/api/meta.ts b/content/api/meta.ts index df1f25e..8923e7e 100644 --- a/content/api/meta.ts +++ b/content/api/meta.ts @@ -12,6 +12,7 @@ export default defineMeta({ "operations", "webhooks", "sdk", + "partners", "reference", ], }); diff --git a/content/api/partners/go-live.mdx b/content/api/partners/go-live.mdx new file mode 100644 index 0000000..3f2c181 --- /dev/null +++ b/content/api/partners/go-live.mdx @@ -0,0 +1,27 @@ +--- +title: Take a partner integration live +description: Prove the customer journey in the test tenant, then promote configuration and verify live traffic +sidebar: + order: 2 +--- + +Use the test and live tenants provisioned for your partner account. Give them separate system Spaces, issuer identifiers, signing keys, webhook secrets, and application configuration. A customer JWT from one mode must not work in the other. + +## Prove the test journey + +1. Publish the test system Space. Wait for its publish operation, then read `manifestStatus.items`. Confirm the issuer and subscription are `active`. +2. Sign in through your own identity system and mint a customer JWT. Create a Space for that external principal, publish a page, follow the operation to completion, and fetch the live URL. +3. Sign in as a second customer. Confirm its Space list excludes the first customer's Space and that it cannot read or mutate the first customer's Space by ID. +4. From a trusted server, list the principals, assign a catalog plan, suspend and unsuspend a test customer, and inspect usage. Test the close flow only on a disposable principal. Closing is terminal. +5. Deliver a signed event to your public HTTPS receiver. Confirm raw-body signature verification, durable duplicate handling, the customer-facing action, and a redelivery after a controlled failure. +6. If you use branded claim, access handoff, site domains, or an API gateway, exercise each customer-facing URL over HTTPS. Check the domain setup and manifest status as well as the served response. + +An SDK build or a `200` from `/health` does not prove these customer and provider steps. Keep the test run's request IDs, operation IDs, event IDs, and served URLs for debugging. + +## Promote configuration + +Publish a live system Space with a live issuer and receiver first. `POST /v1/tenants/{tenantId}/promote`, called with the live tenant's management key, copies the linked test tenant's system configuration through the live system Space. It does not copy customer resources or flip their mode. Both system Spaces need live versions. The live issuer remains its own declaration because an active issuer identifier is globally unique. + +Read the returned live tenant and its manifest status. Recheck each item and make a fresh live customer token. Publish and fetch a live Space, then verify a signed event reaches the live receiver. Switch real customer traffic only after those checks pass. + +Keep the last good system Space version so you can promote it again if a configuration publish fails. That does not undo customer closures, usage, or rotated webhook secrets. [Build the integration](/api/partners) has the setup steps and [the Partner API reference](/platforms/api/reference) has the operation schemas. diff --git a/content/api/partners/index.mdx b/content/api/partners/index.mdx new file mode 100644 index 0000000..46616f5 --- /dev/null +++ b/content/api/partners/index.mdx @@ -0,0 +1,50 @@ +--- +title: Partner API +description: Set up a partner tenant, system Space, customer tokens, billing policy, and event delivery +sidebar: + order: 1 +--- + +The Partner API lets you run your own customer login and product on Spacefast hosting. Your customers are external principals identified by IDs from your identity system. They do not need Spacefast accounts. You own customer billing and support. + +Start with the [partner starter](https://github.com/spacefast/partner-starter). Its root app shows a customer portal and a staff dashboard. The [Express example](https://github.com/spacefast/partner-starter/tree/main/examples/express) uses a smaller server with Clerk or local test authentication. Both work with the [system Space template](https://github.com/spacefast/partner-system-space). Use a test tenant until the whole journey works. + +## Get a tenant and management key + +Ask Spacefast to provision a partner account. Provisioning creates separate live and test tenants and gives you one tenant-rooted `partner_admin` API key for each. Save each secret when it is issued. A team API key cannot grant itself `partner_admin`, and your customer browser must never receive a management key. + +The key's tenant ID is the path ID for `GET /v1/tenants/{tenantId}`. That response reports the tenant's mode, provisioning mode, system Space, brand, and `manifestStatus`. A key can read only its own tenant. You can create narrower tenant keys with `POST /v1/tenants/{tenantId}/api-keys`; creation returns the secret once. Rotate by creating a replacement, switching callers, and revoking the old key. + +## Publish your system Space + +Create a Space with the management key and an external principal reserved for your own configuration, then save `data.space.id` from the response. Publish only the `site/` directory of the [system Space template](https://github.com/spacefast/partner-system-space). Once the Space is active, designate it with `PATCH /v1/tenants/{tenantId}` and `systemSpaceId`. Send the last observed `baseSystemSpaceId` to prevent a concurrent change from replacing your designation. + +The live `sf.jsonc` in that Space declares your presentation, token issuers, webhook subscriptions, plan catalog, browser origins, and optional hostnames. Publishing a new version replaces the declared configuration. Removing a declaration can remove the setting it created, so review the full manifest before each publish. Read `manifestStatus.items` after the publish operation finishes. A successful publish response alone does not mean an issuer, subscription, or domain is active. + +## Activate your token issuer + +Your server owns an Ed25519 private key. Put only the public key and a stable HTTPS issuer identifier in `system.tokenIssuers`. Publish once, then read the `tokenIssuers` manifest item from `GET /v1/tenants/{tenantId}`. It returns a Spacefast audience and a challenge while the issuer is pending. Sign a short-lived proof with your private key, add that proof to the declaration, and publish again. The template and both starters include the key and proof commands. + +After the item becomes `active`, mint a customer JWT from a verified session in your own identity system. Use `EdDSA`, `typ: at+jwt`, the declared `kid` and `iss`, the returned single `aud`, and a normalized lowercase external principal ID as `sub`. Include `client_id`, `jti`, `iat`, and `exp`; the lifetime may not exceed 30 minutes. Renew the JWT before it expires. A customer token is limited to its own principal. It cannot manage the tenant, other customers, or billing. + +Use [the partner SDK](https://www.npmjs.com/package/@spacefast/sdk) through `@spacefast/sdk/partner` or `@spacefast/sdk/partner/query`. Give each principal a separate query cache. Keep the management key on your server; browser calls use only customer `JWTs`. If your browser calls the API directly, add its exact origin to `system.api.allowedOrigins`. + +## Create sites and manage customers + +There is no customer registration endpoint. A customer's first authorized operation creates its external principal. Call `POST /v1/spaces` with `principal: { "type": "external", "id": "" }`, then publish content with `POST /v1/publish`. Follow returned operations until they finish, and check the live URL. `GET /v1/spaces` under that customer's JWT returns only its Spaces. + +Your server can use its tenant key to list and inspect customers with `/v1/principals`. It can assign a plan from the live system catalog with `PUT /v1/principals/external/{principalId}/plan`. A plan assignment changes entitlements for future publishes; already published versions keep the entitlements they had at publish time. It can suspend and unsuspend customers. A partner suspension serves a suspended page and blocks mutations. Closing a customer is terminal; the API refuses a close while the customer owns Spaces unless you explicitly force it. + +The partner owns prices, checkout, invoices, and collection. Read `/v1/usage` for hourly records and `/v1/usage/periods` for monthly totals. Test-tenant usage is measured but never billable. Do not treat an open period as a final invoice total. + +## Receive customer events + +Declare an HTTPS subscription in `system.subscriptions`, then read `manifestStatus` to confirm it is active. The event catalog at `GET /v1/events/catalog` lists the `principal.notification.*` events that you must deliver to your customers. Spacefast does not send a fallback customer email for these partner events. + +Verify `Spacefast-Signature` against the raw request body before parsing it. Store the event ID durably before returning success, then process it with retries and duplicate handling. Keep both secrets during a signing-secret rotation. The starters include a receiver and signature verification; their inbox is a receipt store, so you must add your own notification or billing consumer. [Webhook delivery tools](/api/webhooks) let you inspect and redeliver failures. + +## Add your brand + +Start with `system.presentation`. Add claim and access handoff origins only after those pages can complete their customer flows. Custom site hostnames need DNS ownership and HTTPS serving. A custom API origin also needs an HTTPS gateway that forwards to Spacefast and overwrites `x-spacefast-tenant-id` with your tenant ID. Check `GET /v1/tenants/{tenantId}/api-origin-verification` and actual requests through that gateway. A `pending` manifest item is not an active custom origin. + +Follow the [go-live checks](/api/partners/go-live) before sending real customers to the live tenant. The [Partner API reference](/platforms/api/reference) has the exact request and response schemas. diff --git a/content/api/partners/meta.ts b/content/api/partners/meta.ts new file mode 100644 index 0000000..e48240c --- /dev/null +++ b/content/api/partners/meta.ts @@ -0,0 +1,6 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Partner API", + pages: ["index", "go-live"], +}); diff --git a/content/api/sdk.mdx b/content/api/sdk.mdx index 3f2f1af..592afd9 100644 --- a/content/api/sdk.mdx +++ b/content/api/sdk.mdx @@ -156,6 +156,9 @@ There is no built-in iterator. Walk the cursor yourself, as shown in [Pagination | `@spacefast/sdk/publish` | the publish pipeline | | `@spacefast/sdk/query` | TanStack Query option and mutation factories | | `@spacefast/sdk/openapi.json` | the spec itself | +| `@spacefast/sdk/partner` | typed client for the Partner API | +| `@spacefast/sdk/partner/query` | partner query factories with a separate cache for each principal | +| `@spacefast/sdk/openapi.partner.json` | the Partner API spec | The query layer defaults to browser session cookies against the production API. Point it elsewhere once, before the first request: From ff8ff318c401782cf972b85ea1e12448aaa9e2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?batuhan=20i=C3=A7=C3=B6z?= Date: Wed, 23 Sep 2026 15:05:49 +0200 Subject: [PATCH 2/2] Link partner guides to the deployed API contract --- content/api/partners/go-live.mdx | 2 +- content/api/partners/index.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content/api/partners/go-live.mdx b/content/api/partners/go-live.mdx index 3f2c181..64f02f8 100644 --- a/content/api/partners/go-live.mdx +++ b/content/api/partners/go-live.mdx @@ -24,4 +24,4 @@ Publish a live system Space with a live issuer and receiver first. `POST /v1/ten Read the returned live tenant and its manifest status. Recheck each item and make a fresh live customer token. Publish and fetch a live Space, then verify a signed event reaches the live receiver. Switch real customer traffic only after those checks pass. -Keep the last good system Space version so you can promote it again if a configuration publish fails. That does not undo customer closures, usage, or rotated webhook secrets. [Build the integration](/api/partners) has the setup steps and [the Partner API reference](/platforms/api/reference) has the operation schemas. +Keep the last good system Space version so you can promote it again if a configuration publish fails. That does not undo customer closures, usage, or rotated webhook secrets. [Build the integration](/api/partners) has the setup steps and the [live Partner OpenAPI document](https://api.spacefast.com/openapi.partner.json) has the current operation schemas. diff --git a/content/api/partners/index.mdx b/content/api/partners/index.mdx index 46616f5..bfff2c0 100644 --- a/content/api/partners/index.mdx +++ b/content/api/partners/index.mdx @@ -47,4 +47,4 @@ Verify `Spacefast-Signature` against the raw request body before parsing it. Sto Start with `system.presentation`. Add claim and access handoff origins only after those pages can complete their customer flows. Custom site hostnames need DNS ownership and HTTPS serving. A custom API origin also needs an HTTPS gateway that forwards to Spacefast and overwrites `x-spacefast-tenant-id` with your tenant ID. Check `GET /v1/tenants/{tenantId}/api-origin-verification` and actual requests through that gateway. A `pending` manifest item is not an active custom origin. -Follow the [go-live checks](/api/partners/go-live) before sending real customers to the live tenant. The [Partner API reference](/platforms/api/reference) has the exact request and response schemas. +Follow the [go-live checks](/api/partners/go-live) before sending real customers to the live tenant. The [live Partner OpenAPI document](https://api.spacefast.com/openapi.partner.json) has the current request and response schemas.