Partner program

Partner API reference

Provision clients, poll builds, and create checkout links programmatically.

Programmatically provision client sites, poll build status, and create guest checkout links. The API mirrors the partner portal provisioning flow — the same AI build pipeline runs whether you use the portal or POST /api/partner/v1/clients.

Base URL: https://nurturesite.com/api/partner/v1

Authentication

Send a bearer token on every request:

Authorization: Bearer <token>

X-Partner-Api-Key: <api key> is accepted as an alternative header for API keys.

API keys (nsk_partner_…) are for server-to-server integrations. Create them under Partner → API & integrations → Create API key: give the key a label, tick the scopes it needs, and pick how long it lasts (30 days, 90 days — the default — or 1 year). The full key is shown once, right after you create it — copy it then. NurtureSite only stores a hash, so a lost key can't be recovered; revoke it and create a new one.

OAuth 2.0 + PKCE is for third-party apps that act on your partner account after you approve them (see OAuth below).

Your partner account must be active (invite accepted, not suspended) for any key or token to work. A key or token also stops working if the partner login that created it is disabled or is no longer your account's partner login.

Partner Agreement acceptance

When NurtureSite publishes a Partner Agreement version you haven't accepted yet, every call with your API keys and OAuth tokens gets 403 AGREEMENT_ACCEPTANCE_REQUIRED until you accept it in the partner portal (signing in opens the agreement first, at /partner/agreement). We email your partner login when a new version is published. Nothing is revoked: as soon as you accept, the same keys and tokens work again. Accepting the agreement on your invite page counts, so a new partner isn't asked twice.

{
  "error": "AGREEMENT_ACCEPTANCE_REQUIRED",
  "message": "Accept the current Partner Agreement in the partner portal to keep using the API: https://nurturesite.com/partner/agreement",
  "agreement_url": "https://nurturesite.com/partner/agreement",
  "agreement_version": "2.0"
}

Creating, rotating and revoking keys and disconnecting apps are recorded in NurtureSite's audit log. NurtureSite can also revoke a key or disconnect an app for you (for example, if a key leaks) without suspending your account.

Key expiry and rotation

  • Every new API key has an expiry date, shown next to the key under API & integrations. Keys expiring within 14 days are flagged Expiring soon.
  • After that date, calls with the key get 401 API_KEY_EXPIRED. Expired keys stay listed for 30 days so you can see what happened; Remove clears one sooner.
  • NurtureSite emails your partner login 14 days and 3 days before a key expires (once each).
  • Rotate issues a new key with the same scopes and label (plus "(rotated)") and the lifetime you pick, shown once like a new key. The old key keeps working for 7 more days — or until its own expiry, if that is sooner — so you can switch your integration over without downtime. A rotated key can't be rotated again; rotate its replacement instead. Rotating works even at the API key limit, as long as fewer than twice that many keys are live (old keys still in their 7-day overlap count).
  • Keys created before keys expired have no expiry date and keep working. They are marked Rotate recommended: rotating one gives you a key that expires and retires the old one after 7 days.
  • Revoke a key at any time to stop it immediately.

Scopes

Scope Purpose Pre-selected on new API keys
clients:read List clients you have provisioned Yes
clients:write Create a client + queue a site build; edit or release a client Yes
sites:read List sites and poll deployment_status Yes
checkout:write Create guest Stripe checkout links; change the monthly price of a paid site No — opt in

clients:write provisions the site but does not bill the client. After the build deploys, create a checkout link with checkout:write (or use the partner portal pricing page).

checkout:write is never granted by default. Tick it when creating an API key, or have your OAuth app request it explicitly. Keep keys with checkout:write on your server only.

Discovery

GET /api/partner/v1/meta
Authorization: Bearer <token>

Works with any valid key or token, whatever its scopes. Returns the scopes on your token, your account status (connect_ready, upfront_platform_fee_cents, checkout_minimums), industry presets, scope details, field reference, error codes, rate limits, and the endpoint list. Call this first when integrating.

token.expires_at is when this key or token stops working (null for keys created before keys expired).

Sites are identified by uuid

Every site has a uuid. All site paths use it: /sites/{uuid}. Responses also include a numeric id, but it is informational only — /sites/{id} returns 404 PROJECT_NOT_FOUND. Each site object includes its own API url.

Provision a client site

POST /api/partner/v1/clients
Authorization: Bearer <token>
Content-Type: application/json

Required

  • name — contact name
  • email — client email (no portal login is created)
  • company — business name (defaults to name)
  • project_name — site title (defaults to company)
  • prompt — build instructions for the AI pipeline
  • preset — industry template: contractor, agency, restaurant, medical, legal, real_estate, fitness, salon, education, or other

Customization

Send hints as top-level fields or inside a customization object (same keys, same validation):

Field Description
source_url Existing website URL to scrape when rebuilding (top level only). Requires ownership_attestation: true
ownership_attestation true confirms your client authorized you to use their business name, website and brand assets. Required with source_url (422 OWNERSHIP_ATTESTATION_REQUIRED otherwise); recorded with the site (top level only)
tone Copy tone (professional, friendly, bold, warm)
visual_mood Visual style direction for the generated theme
layout_focus Page structure emphasis
services Array or comma-separated list of services
service_area Geographic area served
brand_primary / brand_accent Colors for new businesses, as #RRGGBB
preview_password Password for the preview site, 4–64 characters (top level only)
preferences Advanced: see below

If prompt is omitted, NurtureSite builds one from your customization fields. Unknown keys inside customization are ignored.

preferences is merged on top of the preferences built from your customization. Only these keys are accepted; anything else is ignored: business_maturity, has_website, tone, visual_mood, layout_focus, brand (primary, accent, secondary as #RRGGBB), and customer_intake (services, service_area, contact_phone, contact_email, years_in_business, differentiator, business_idea, target_customers, launch_timeline, must_haves, competitor_inspiration).

Preview password

Preview sites are always password protected. If you send preview_password, that is the password. If you don't, NurtureSite generates one and returns it once in provisioning.preview_password — store it and share it with your client. It is stored hashed and can't be retrieved later. When you set your own, the response returns preview_password: null.

Example

{
  "name": "Jane Smith",
  "email": "jane@acmeplumbing.com",
  "company": "Acme Plumbing",
  "project_name": "Acme Plumbing",
  "preset": "contractor",
  "source_url": "https://old-acme-plumbing.com",
  "ownership_attestation": true,
  "prompt": "Modern, trustworthy site for a family plumbing company.",
  "tone": "friendly",
  "services": ["Emergency plumbing", "Water heaters", "Drain cleaning"],
  "service_area": "Salt Lake Valley"
}

Response (201)

{
  "client": { "id": 1, "name": "...", "company": "...", "email": "..." },
  "site": {
    "uuid": "9b2f6c1e-…",
    "id": 1,
    "name": "Acme Plumbing",
    "slug": "acme-plumbing",
    "subscription_status": "preview",
    "deployment_status": "queued",
    "preview_url": "https://acme-plumbing.sites.example.com",
    "url": "https://nurturesite.com/api/partner/v1/sites/9b2f6c1e-…"
  },
  "provisioning": {
    "preset": "contractor",
    "source_url": "https://old-acme-plumbing.com",
    "preview_password_set": true,
    "preview_password": "generated-once",
    "deployment_status": "queued",
    "poll_url": "https://nurturesite.com/api/partner/v1/sites/9b2f6c1e-…",
    "next_steps": ["..."]
  }
}

Poll GET /api/partner/v1/sites/{uuid} (the poll_url) until deployment_status is deployed (or failed).

Each client needs its own email. If a NurtureSite account or client already uses it — including one of your own clients — provisioning returns 409 CLIENT_EMAIL_IN_USE; look the client up with GET /clients instead of retrying. Provisioning requests for one partner account run one at a time; a request that can't start returns 409 DEPLOYMENT_IN_PROGRESS, so retry after a few seconds.

List clients and sites

GET /api/partner/v1/clients?page=1&per_page=25
GET /api/partner/v1/sites?page=1&per_page=25
GET /api/partner/v1/sites/{uuid}

per_page is between 1 and 100 (default 25). Responses include meta.current_page, meta.last_page, meta.per_page and meta.total.

GET /sites/{uuid} also returns the site's client and its latest checkout link: id, status (pending, paid, void, or expired for an unpaid link past its expiry), upfront_retail_cents, monthly_retail_cents, expires_at, and checkout_url. checkout_url is the unpaid, unexpired link you issued, so you can send it again; it is null once the link is paid, replaced, cancelled or expired, and for links created before links were kept.

GET /clients lists your clients. Leads you imported in the partner portal are not clients until you create a checkout link for them (or they pay), so they aren't listed — their demo sites do appear in GET /sites.

Edit a client

PATCH /api/partner/v1/clients/{client_id}
Authorization: Bearer <token>
Content-Type: application/json

{"name": "Jane Doe", "company": "Acme Dental", "email": "office@acmedental.com"}

Requires clients:write. Send only the fields you want to change. client_id is the id from GET /clients or POST /clients. The email can change only until the client opens a checkout link, has a NurtureSite login, or has a login invite waiting (409 CLIENT_EMAIL_LOCKED then), and it can't be one another account or client already uses (409 CLIENT_EMAIL_IN_USE). The response returns the client and the list of changed fields. Edits are recorded in the same audit log as portal edits.

Release a client

POST /api/partner/v1/clients/{client_id}/release
Authorization: Bearer <token>
Content-Type: application/json

{"reason": "Client is moving in-house"}

Requires clients:write. Hands the client over to NurtureSite, the same as Release client on the client's page in the portal. It can't be undone through the API.

  • The client's sites keep running, and the client keeps paying the same price: the existing subscription continues, billed by NurtureSite directly. Nothing is charged again and the client doesn't re-enter a card.
  • Your commission on the client stops immediately. Commission already recorded (including owed or pending payouts) is not affected.
  • Your access ends: the client and its sites disappear from GET /clients and GET /sites, and your site admin login on its sites is removed. Each site's built-in admin account (admin@…local) also gets a new password that only the client can see, so a password you chose or saw no longer works.
  • Unpaid checkout links for the client are cancelled; their pay pages stop working.
  • You and the client are both emailed.

reason is optional (up to 1000 characters) and only visible to NurtureSite. Imported leads can be released too.

Response (200):

{
  "client": {"id": 42, "name": "Jane Doe", "company": "Acme Dental", "email": "office@acmedental.com"},
  "released": true,
  "released_at": "2026-10-02T15:04:05+00:00",
  "commission": "stopped",
  "billing": "unchanged",
  "voided_checkouts": 1,
  "sites": [
    {"uuid": "9b1d…", "name": "Acme Dental", "subscription_status": "active", "site_admin_access": "revocation_queued"}
  ]
}

site_admin_access is revocation_queued for deployed sites (your login is removed in the background) and none for sites that were never deployed. A client that isn't yours, or was already released, returns 404 CLIENT_NOT_FOUND; a second release while the first is still running returns 409 DETACH_IN_PROGRESS.

Checkout

Requires the checkout:write scope and completed Stripe Connect onboarding (account.connect_ready in GET /meta).

POST /api/partner/v1/sites/{uuid}/checkout
{
  "upfront_retail_cents": 150000,
  "monthly_retail_cents": 4900
}

Returns checkout_url to send your client, plus checkout_id and expires_at. Commission rates are snapshotted on the checkout link. Creating a new link replaces (voids) the site's earlier unpaid links. A site that has already been paid for, or has an active subscription, can't get a new link (CHECKOUT_ALREADY_PAID); change its monthly price with POST /sites/{uuid}/price instead. A site NurtureSite has suspended can't be sold until it is restored (422 SITE_SUSPENDED).

Prices are in cents. upfront_retail_cents must be at least $75.00 (7500) and at least your platform setup fee; monthly_retail_cents must be at least the platform's monthly minimum. Your exact floors are in account.checkout_minimums from GET /meta, and a price below one returns 422 with minimum_cents.

Change the monthly price of a paid site

Requires the checkout:write scope. For a site your client has already paid for through one of your checkout links; an unpaid site is priced with POST /sites/{uuid}/checkout instead.

POST /api/partner/v1/sites/{uuid}/price
{
  "monthly_retail_cents": 7900
}

The client's existing subscription moves to the new price straight away (no new checkout link). Stripe prorates it: the client's next invoice includes a credit for the unused days of the current period at the old price and a charge for the same days at the new price. The client is emailed about the change (replies go to your support email), and you get a confirmation email. Your hosting commission follows the new price. The setup price can't change after payment.

monthly_retail_cents must be at least the platform's monthly minimum (account.checkout_minimums.monthly_cents in GET /meta); a lower price returns 422 MONTHLY_BELOW_MINIMUM with minimum_cents.

Response (200)

{
  "site_uuid": "9b2f0c4e-…",
  "monthly_retail_cents": 7900,
  "previous_monthly_retail_cents": 4900,
  "effective_at": "2026-10-02T15:04:05+00:00",
  "next_invoice_at": "2026-10-20T00:00:00+00:00",
  "proration": "create_prorations",
  "price_change_id": 12
}

next_invoice_at is the end of the current billing period, when the prorated adjustment is invoiced (null if Stripe didn't say).

Errors

Code HTTP Meaning
VALIDATION_ERROR 422 monthly_retail_cents missing or not a whole number of cents
MONTHLY_BELOW_MINIMUM 422 Below the platform minimum — see minimum_cents
MONTHLY_ABOVE_MAXIMUM 422 Above Stripe's largest amount
PRICE_UNCHANGED 422 That is already the site's monthly price
SITE_NOT_PAID 422 The client hasn't paid for this site through one of your checkout links
SUBSCRIPTION_NOT_ACTIVE 422 The site has no active or past-due subscription
SUBSCRIPTION_ENDING 422 The subscription is set to end (the client cancelled, or NurtureSite scheduled the end of term)
SITE_SUSPENDED 422 NurtureSite suspended this site; contact support
SITE_CANCELLED 422 NurtureSite cancelled this site; contact support
CLIENT_NOT_OWNED 422 Client no longer managed by your account
PARTNER_SUSPENDED 403 Account suspended
REPRICE_BUSY 409 Another price change for this site is running; retry shortly
SUBSCRIPTION_NOT_REPRICEABLE 409 The site's subscription wasn't created by your checkout link (or no longer bills its hosting price); contact support
STRIPE_ERROR 502 Stripe refused or didn't complete the change. When Stripe rejected the request, nothing was changed and the message includes Stripe's reason; otherwise retry in a few minutes (a retry is safe)
REPRICE_UNAVAILABLE 503 Price changes aren't available yet; retry later
PRICE_CHANGE_FAILED 500 The change failed on our side and may already have reached Stripe; retrying the same request is safe and completes it

A site that isn't yours, or whose client has left your partner account, returns 404 PROJECT_NOT_FOUND.

What the API does not cover

These are handled in the partner portal or site admin after deploy:

  • Custom domains — your client connects their domain from their own NurtureSite login
  • Site admin SSO — open admin from the partner portal
  • Lead CSV import — partner portal only
  • Theme edits, content, CRM — site admin after handoff
  • Changing the preview password — not available via the API yet
  • A second site for an existing client — use Add another site on the client's page in the portal (POST /clients always creates a new client)
  • Stripe Connect onboarding — partner portal

You manage your clients' sites as their agency partner. After their first payment, clients are emailed an invite to their own NurtureSite login, where they can see billing and invoices and, if they choose, leave your partner account (the same outcome as a release).

OAuth

Metadata: GET /api/partner/oauth/.well-known/authorization-server

OAuth clients are public clients (no client secret) and must use PKCE with S256.

  1. Get a client. If the metadata lists a registration_endpoint, register with POST /api/partner/oauth/register (client_name, redirect_uris), authenticated with one of your API keys that has the clients:read scope. The client belongs to your partner account. If no registration_endpoint is listed, client registration isn't open yet — contact support, or use an API key with checkout:write instead.

    Registration errors use the OAuth shape {"error": "...", "error_description": "..."}: 401 invalid_token (missing or invalid key), 403 insufficient_scope (the key lacks clients:read), 403 access_denied (account suspended or not active yet, a Partner Agreement version waiting for your acceptance, or registration turned off), 429 slow_down (too many requests or registrations; see Retry-After), and 400 invalid_redirect_uri / invalid_client_metadata for a bad body.

  2. Authorize. Send the partner's browser to authorization_endpoint with client_id, an exactly matching registered redirect_uri, code_challenge, code_challenge_method=S256, state, and the scope you need (space-separated). The partner sees a consent screen listing exactly the scopes that will be granted and clicks Authorize or Deny.

    • Unknown scopes are dropped; if none of the requested scopes exist, the app gets error=invalid_scope.
    • No scope grants clients:read clients:write sites:read. Request checkout:write explicitly if you need it.
    • Deny redirects back with error=access_denied.
    • An unknown client_id or unregistered redirect_uri shows an error page and never redirects.
  3. Exchange the code at token_endpoint (grant_type=authorization_code, code, client_id, redirect_uri, code_verifier). Codes expire after 10 minutes and work once.

  4. Refresh with grant_type=refresh_token, refresh_token, and client_id. Refresh tokens rotate: each refresh returns a new pair and the old pair stops working. Re-using an old refresh token revokes the app's tokens, and the partner must authorize again.

  5. Authorizing again replaces the app's earlier grant: once the new code is exchanged, the access and refresh tokens from the partner's earlier authorizations of the same app stop working (they get 401 and invalid_grant), and the app keeps only the new pair, with the newly granted scopes. Other apps on the partner account are not affected.

Access tokens last 60 minutes; refresh tokens last 90 days.

Token endpoint errors follow RFC 6749 — {"error": "...", "error_description": "..."} with invalid_request, invalid_grant or unsupported_grant_type — and responses are sent with Cache-Control: no-store. Suspended or pending partner accounts get invalid_grant. While a Partner Agreement version is waiting for your acceptance, the consent screen opens the agreement first (so no new authorizations), but refreshing an app's existing tokens keeps working so the connection survives; its API calls get 403 AGREEMENT_ACCEPTANCE_REQUIRED until you accept. A 5xx from the token endpoint is a temporary failure on our side: keep the refresh token and retry it later.

Partners can see and disconnect authorized apps under Partner → API & integrations. Disconnecting revokes every access and refresh token the app holds.

Rate limits

  • 60 requests per minute per partner account, across all of its keys and tokens (429 RATE_LIMITED, with a Retry-After header).
  • 300 requests per minute per IP address on /api/partner/v1.

Errors

Errors are JSON: {"error": "CODE", "message": "..."}. Some add hint, required_scope, errors (validation) or minimum_cents.

A 5xx response means something failed on our side; your key is fine, so retry later. Authentication problems are always 401/403 with one of the codes below.

Code HTTP Meaning
UNAUTHORIZED 401 Invalid or missing token
API_KEY_EXPIRED 401 API key past its expiry date — rotate it or create a new one
INSUFFICIENT_SCOPE 403 Token lacks the scope in required_scope
PARTNER_SUSPENDED 403 Account suspended
PARTNER_NOT_ACTIVE 403 Account not active yet (invite not accepted, or under review)
AGREEMENT_ACCEPTANCE_REQUIRED 403 You haven't accepted the current Partner Agreement version; accept it in the portal (agreement_url), then retry
API_DISABLED 403 Partner API temporarily disabled
RATE_LIMITED 429 Too many requests for your partner account
VALIDATION_ERROR 422 Invalid body — see errors
CLIENT_EMAIL_IN_USE 409 A NurtureSite account or client (yours included) already uses this email
CLIENT_EMAIL_LOCKED 409 Client started checkout, has an account or has a login invite waiting; their email can't change (client edits)
CLIENT_NOT_FOUND 404 No client with this id on your account (client edits and releases)
DETACH_IN_PROGRESS 409 This client is already being released; retry shortly
DEPLOYMENT_IN_PROGRESS 409 Another provisioning request for your account is running; retry shortly
PARTNER_SITE_LIMIT_REACHED 402 Site limit reached
PREVIEW_BUILD_LIMIT_REACHED 402 This month's free preview builds are used up; paid sites don't count, resets on the 1st (UTC) (provisioning)
OWNERSHIP_ATTESTATION_REQUIRED 422 source_url sent without ownership_attestation: true (provisioning)
PROMPT_REQUIRED 422 No prompt and no customization describing the business (provisioning)
PROVISION_INVALID_INPUT 422 The details couldn't be turned into a site; check the fields against /meta (provisioning)
PROVISION_FAILED 500 Provisioning failed on our side; check GET /clients before retrying (provisioning)
CLIENT_UPDATE_FAILED 500 The edit failed on our side; retry later (client edits)
PROJECT_NOT_FOUND 404 No site with this uuid on your account
CONNECT_NOT_READY 402 Stripe Connect incomplete (checkout)
UPFRONT_BELOW_FLOOR 422 Upfront price below your platform setup fee — see minimum_cents (checkout)
UPFRONT_BELOW_MINIMUM 422 Upfront price below the platform minimum — see minimum_cents (checkout)
MONTHLY_BELOW_MINIMUM 422 Monthly price below the platform minimum — see minimum_cents (checkout, price changes)
CHECKOUT_ALREADY_PAID 422 Site already paid or subscribed (checkout)
CLIENT_NOT_OWNED 422 Client no longer managed by your account (checkout, price changes)
SITE_SUSPENDED 422 NurtureSite suspended this site; contact support (checkout, price changes)
SITE_CANCELLED 422 NurtureSite cancelled this site; contact support (price changes)
SITE_NOT_PAID 422 Site not paid through one of your checkout links (price changes)
SUBSCRIPTION_NOT_ACTIVE 422 Site has no active subscription (price changes)
SUBSCRIPTION_ENDING 422 Site's subscription is set to end (price changes)
MONTHLY_ABOVE_MAXIMUM 422 Monthly price above Stripe's largest amount (price changes)
PRICE_UNCHANGED 422 Already the site's monthly price (price changes)
REPRICE_BUSY 409 Another price change for the site is running; retry shortly (price changes)
SUBSCRIPTION_NOT_REPRICEABLE 409 Subscription can't be repriced through the API; contact support (price changes)
STRIPE_ERROR 502 Stripe refused or didn't complete the change (price changes)
REPRICE_UNAVAILABLE 503 Price changes not available yet; retry later (price changes)
PRICE_CHANGE_FAILED 500 Price change failed on our side (price changes)

Full list in GET /api/partner/v1/meta.