← Developers/Medourav1.0.0
Medoura · REST API

Medoura Landing Page API

Read-only API that lets a tenant pull its Medoura branding, content, categories, products and pricing into a custom landing page. Internal and admin routes are not part of this public contract and are never included here.

The Medoura Landing Page API is a read-only API for building a custom landing page that shows your own live branding, categories, products, pricing and home-page content, then sends visitors into your Medoura checkout.

Base URLhttps://medoura.co

Overview

Every request is made with an API key that belongs to one practice, so each response contains only that practice's data. The API is read-only: it never creates orders or collects patient information. Checkout stays in Medoura.

To send a visitor to checkout, link to your own Medoura `/start` page. Each product carries a `start_url_path` (for example `/start?medication_family=semaglutide`). It is a path, not a full URL: prefix it with your own Medoura domain, such as `https://your-practice-domain.com` + `start_url_path`.

The Landing Page API is an add-on. Until it is enabled for your account, every data endpoint returns `403 feature_not_enabled`.

Quickstart

Create a key in Settings > API (`/staff/settings/api`), then call each endpoint from your server. Replace the host with your own Medoura domain.

export MEDOURA_API_KEY="med_live_..."
export MEDOURA_HOST="https://your-practice-domain.com"

# Branding: name, logo, colors
curl -H "Authorization: Bearer $MEDOURA_API_KEY" "$MEDOURA_HOST/api/v1/branding"

# Categories: the treatment categories you offer
curl -H "Authorization: Bearer $MEDOURA_API_KEY" "$MEDOURA_HOST/api/v1/categories"

# Products, optionally filtered to one category
curl -H "Authorization: Bearer $MEDOURA_API_KEY" "$MEDOURA_HOST/api/v1/products?category=weightLoss"

# Pricing, plans and promotions
curl -H "Authorization: Bearer $MEDOURA_API_KEY" "$MEDOURA_HOST/api/v1/pricing"

# Published home-page content, in English or Spanish
curl -H "Authorization: Bearer $MEDOURA_API_KEY" "$MEDOURA_HOST/api/v1/content/home?locale=en"

Category keys are camelCase (for example `weightLoss`). Take the `key` values from `GET /api/v1/categories` rather than guessing; the key in the example above is illustrative.

Authentication

Send your API key in the `Authorization` header on every request:

Authorization: Bearer med_live_...

Keys are created in Settings > API at `/staff/settings/api`. A key is shown exactly once, when it is created, so copy it immediately. A practice can have up to 10 active keys.

To rotate a key with no downtime: create a new key, switch your server over to it, then revoke the old one.

Use your key from your server only. The API does not send CORS headers, so browsers cannot call it directly, and a key shipped in browser code is a leaked key. Fetch on your server (or at build time) and pass the data to your page.

Rate Limits

WindowLimit per key
Per minute60 requests
Per day5,000 requests

Over the limit, the API returns `429 rate_limited` with a `Retry-After` header giving the seconds to wait. Requests count against both limits even when they end in `304 Not Modified`. Requests with a missing or invalid key are limited separately, to 20 per minute per IP address, and also answer `429` with `Retry-After`. Combine the caching below with your own server-side cache so a busy landing page does not spend your budget on every visit.

Caching

Responses carry `Cache-Control: private, max-age=<seconds>, stale-while-revalidate=<seconds>` (both values equal the TTL below), `Vary: Authorization` and a weak `ETag`. A cache may serve a response for up to the TTL and then, while it revalidates, up to the same time again, so data can be up to about twice the TTL old. Send the ETag back in `If-None-Match` and an unchanged response returns `304 Not Modified` with no body.

Endpointmax-age
/api/v1/branding600 s
/api/v1/categories600 s
/api/v1/products600 s
/api/v1/content/{pageKey}60 s
/api/v1/pricing60 s
curl -i -H "Authorization: Bearer $MEDOURA_API_KEY" \
  -H 'If-None-Match: W/"<etag from the previous response>"' \
  "$MEDOURA_HOST/api/v1/products"
Turning the add-on on or off can take up to 5 minutes to take effect.

Errors

Every error uses one shape:

{
  "error": {
    "code": "validation_failed",
    "message": "Invalid locale",
    "details": [{ "field": "locale", "message": "Must be \"en\" or \"es\"" }]
  }
}

`details` appears only on validation errors.

StatusCodeMeaning
401invalid_keyThe key is missing, invalid or revoked.
403feature_not_enabledThe Landing Page API is not enabled for this account. Enabling the add-on can take up to 5 minutes to take effect, and turning it off likewise.
403account_lockedThe account is locked or otherwise unavailable (for example for billing). Contact support. A lock or unlock can take up to about a minute to take effect.
404not_foundUnknown category or page key.
422validation_failedA query parameter is invalid, such as an unsupported locale.
429rate_limitedToo many requests. Wait for `Retry-After` seconds.
500internal_errorUnexpected server error. Retry with backoff.

Filtering by Category

Pass `?category=<key>` to `/api/v1/products` or `/api/v1/pricing` to narrow the response to one category. Use a key returned by `/api/v1/categories`; an unknown key returns `404 not_found`.

On `/api/v1/pricing` the filter applies to `products` only. `promos`, `nutritionProducts`, `nutritionBundles` and `stateCoverage` are not category-filtered and are always returned in full.

Branding Values May Be Null

`GET /api/v1/branding` only returns what the practice has configured. Any field the practice has not set is `null`, never a Medoura or platform default, and the `colors` object is always present with each color either a CSS color string or `null`.

A few fields are derived from the practice's own data: `url` and `nppUrl` come from the practice's domain, `shortName`, `legalName` and `legalFooterCopy` fall back to the practice's own `name`, and the LegitScript URLs come from its seal id. `heroBottleShadowUrl` is reserved and always `null`.

Check every value for `null` before using it, and supply your own fallback (your agency's or the brand's own assets) rather than assuming a logo, color or name is present.

Content

In v1 the only content page is `home`: `GET /api/v1/content/home`. Any other page key returns `404 not_found`. If the practice has no published home content, `content` is `null`. Add `?locale=en` or `?locale=es` to choose the language (default `en`). Only published content is returned; drafts are never exposed.

Versioning

The API is versioned in the path (`/api/v1`). Within v1, changes are additive only: new fields and endpoints may appear, but existing fields are not removed or renamed. Ignore fields you do not recognize.

Branding

1 endpoint
get/api/v1/brandingGet tenant branding

The tenant's brand identity: names, contact details, legal links and imagery. Light and dark variants are provided for logos and colors. Only values the tenant has configured (or that derive from the tenant's own domain or LegitScript seal id) are returned. Every field the tenant has not configured is null, never a Medoura or platform default, so check for null before use. Requires `Authorization: Bearer <Medoura API key>`. The key resolves to exactly one tenant -- no request names a tenant.

Responses
200The tenant's branding. Every field is a string or null. New fields may be added over time.
namestringnulloptional

Display name. Null when the tenant has not configured it.

shortNamestringnulloptional

Short name. Null when the tenant has not configured it. Falls back to the tenant's own `name` when only that is set.

legalNamestringnulloptional

Legal entity name. Null when the tenant has not configured it. Falls back to the tenant's own `name` when only that is set.

operatingStatestringnulloptional

Null when the tenant has not configured it.

urlstringnulloptional

The tenant's production site URL, derived from its own configured domain. Null when the tenant has no usable domain.

shopUrlstringnulloptional

Null when the tenant has not configured it.

cdnUrlstringnulloptional

Base URL for the tenant's hosted assets. Null when the tenant has not configured it.

logoLightUrlstringnulloptional

Logo for light backgrounds. Null when the tenant has not configured it. Absolute http(s) URL.

logoDarkUrlstringnulloptional

Logo for dark backgrounds. Null when the tenant has not configured it. Absolute http(s) URL.

symbolLogoUrlstringnulloptional

Symbol mark for light backgrounds. Null when the tenant has not configured it. Absolute http(s) URL.

symbolLogoDarkUrlstringnulloptional

Symbol mark for dark backgrounds. Null when the tenant has not configured it. Absolute http(s) URL.

faviconUrlstringnulloptional

Favicon. Null when the tenant has not configured it. Absolute http(s) URL.

supportEmailstringnulloptional

Null when the tenant has not configured it.

supportPhonestringnulloptional

Null when the tenant has not configured it.

supportPhoneDisplaystringnulloptional

Null when the tenant has not configured it.

privacyContactEmailstringnulloptional

Null when the tenant has not configured it.

privacyContactPhonestringnulloptional

Null when the tenant has not configured it.

privacyEffectiveDatestringnulloptional

ISO date (YYYY-MM-DD) the Notice of Privacy Practices took effect. Null when the tenant has not configured it.

billingContactEmailstringnulloptional

Null when the tenant has not configured it.

billingContactPhonestringnulloptional

Null when the tenant has not configured it.

emergencyInstructionsstringnulloptional

Null when the tenant has not configured it.

nppUrlstringnulloptional

Notice of Privacy Practices URL. The tenant's own value when configured; otherwise derived from the tenant's domain (its hosted privacy-policy page); null when neither exists.

patientTermsUrlstringnulloptional

Null when the tenant has not configured it.

telehealthConsentUrlstringnulloptional

Null when the tenant has not configured it.

paymentTermsUrlstringnulloptional

Null when the tenant has not configured it.

communicationsUrlstringnulloptional

Null when the tenant has not configured it.

additionalDocumentsUrlstringnulloptional

Null when the tenant has not configured it.

twitterHandlestringnulloptional

Null when the tenant has not configured it.

trustpilotUrlstringnulloptional

Null when the tenant has not configured it.

legitscriptSealIdstringnulloptional

Null when the tenant has not configured it.

legitscriptSealUrlstringnulloptional

Derived from the tenant's own `legitscriptSealId`. Null when no seal id is set.

legitscriptCheckerUrlstringnulloptional

Derived from the tenant's own `legitscriptSealId` and `url`. Null when either is missing.

ogImageUrlstringnulloptional

Social-sharing image. Null when the tenant has not configured it. Absolute http(s) URL.

influencerAvatarUrlstringnulloptional

Avatar image. Null when the tenant has not configured it. Absolute http(s) URL.

legalFooterCopystringnulloptional

Copyright line. The tenant's own value when configured; otherwise derived as "© <legalName>. All rights reserved." from the tenant's own name; null when the tenant has no name.

heroBottleShadowUrlstringnulloptional

Reserved. Hero illustration shadow image. Not tenant-configurable, so always null.

heroBottleUrlstringnulloptional

Hero image. Null when the tenant has not configured it. Absolute http(s) URL.

heroBottleRightUrlstringnulloptional

Right-side hero image. Defaults to `heroBottleUrl` when only that is set; null when neither is set. Absolute http(s) URL.

colorsobjectoptional

Brand colors as CSS color strings. The object is always present; each member is null when the tenant has not configured that color. `dark` is the dark-variant color.

primarystringnulloptional
darkstringnulloptional
creamstringnulloptional
accentstringnulloptional
badgeBgstringnulloptional
badgeTextstringnulloptional
bgGradient1stringnulloptional
bgGradient2stringnulloptional
401`invalid_key` -- the API key is missing, unknown, or revoked.
PublicApiError
403`feature_not_enabled` -- the Landing Page API is not enabled for this tenant. `account_locked` -- the account is locked or otherwise unavailable (for example for billing); contact support. A lock or unlock can take up to about a minute to take effect.
PublicApiError
429`rate_limited` -- 60 requests per minute and 5,000 per day per key. Includes a `Retry-After` header.
PublicApiError
500`internal_error` -- unexpected server error.
PublicApiError

Catalog

3 endpoints
get/api/v1/categoriesList product categories

The tenant's active product categories, in display order, with labels in the requested locale. Requires `Authorization: Bearer <Medoura API key>`. The key resolves to exactly one tenant -- no request names a tenant.

Parameters
localeoptional
queryLabel language. Defaults to `en`.
Responses
200The tenant's categories.
categoriesobject[]required
401`invalid_key` -- the API key is missing, unknown, or revoked.
PublicApiError
403`feature_not_enabled` -- the Landing Page API is not enabled for this tenant. `account_locked` -- the account is locked or otherwise unavailable (for example for billing); contact support. A lock or unlock can take up to about a minute to take effect.
PublicApiError
422`validation_failed` -- `locale` is not `en` or `es`.
PublicApiError
429`rate_limited` -- 60 requests per minute and 5,000 per day per key. Includes a `Retry-After` header.
PublicApiError
500`internal_error` -- unexpected server error.
PublicApiError
get/api/v1/pricingGet pricing

The tenant's full pricing catalog. It mirrors what the tenant's own `/plans` page data (`/api/pricing-data`) contains, including the Stripe price ids on plans and nutrition variants. Promotions are an allowlisted projection: inactive promotions and Stripe promotion or coupon ids are not exposed. `?category` filters only `products`; `promos`, `nutritionProducts`, `nutritionBundles` and `stateCoverage` are NOT category-filtered and are always returned in full. Requires `Authorization: Bearer <Medoura API key>`. The key resolves to exactly one tenant -- no request names a tenant.

Parameters
categoryoptional
queryOnly return `products` in this category. Must be a `key` from `GET /api/v1/categories`; an unknown key returns 404 `not_found`. Does not filter `promos`, `nutritionProducts`, `nutritionBundles` or `stateCoverage`.
Responses
200The tenant's pricing data.
productsobject[]required

Plans with checkout options and prices. The only list that `?category` filters.

promosobject[]required

The tenant's active promotions (not category-filtered). Inactive promotions are omitted. An allowlisted projection: Stripe promotion and coupon ids are not exposed. Fields that are unset are omitted.

nutritionProductsobject[]required

Nutrition shop products (not category-filtered). An allowlisted projection: internal fields such as tenant id, supplier SKU and row ids are not exposed.

nutritionBundlesobject[]required

Nutrition shop bundles (not category-filtered). Same fields as `nutritionProducts` plus `components`.

stateCoverageobject[]required

Which pharmacies serve each US state (not category-filtered).

401`invalid_key` -- the API key is missing, unknown, or revoked.
PublicApiError
403`feature_not_enabled` -- the Landing Page API is not enabled for this tenant. `account_locked` -- the account is locked or otherwise unavailable (for example for billing); contact support. A lock or unlock can take up to about a minute to take effect.
PublicApiError
404`not_found` -- `category` is not one of the tenant's category keys.
PublicApiError
429`rate_limited` -- 60 requests per minute and 5,000 per day per key. Includes a `Retry-After` header.
PublicApiError
500`internal_error` -- unexpected server error.
PublicApiError
get/api/v1/productsList products

The tenant's published medication products, each with the path of its enrollment flow on the tenant's own Medoura host (`start_url_path`) and its plan templates. Optionally filtered to one category. Requires `Authorization: Bearer <Medoura API key>`. The key resolves to exactly one tenant -- no request names a tenant.

Parameters
categoryoptional
queryOnly return products in this category. Must be a `key` from `GET /api/v1/categories`; an unknown key returns 404 `not_found`. Omit to return every product.
Responses
200The tenant's products.
productsobject[]required
401`invalid_key` -- the API key is missing, unknown, or revoked.
PublicApiError
403`feature_not_enabled` -- the Landing Page API is not enabled for this tenant. `account_locked` -- the account is locked or otherwise unavailable (for example for billing); contact support. A lock or unlock can take up to about a minute to take effect.
PublicApiError
404`not_found` -- `category` is not one of the tenant's category keys.
PublicApiError
429`rate_limited` -- 60 requests per minute and 5,000 per day per key. Includes a `Retry-After` header.
PublicApiError
500`internal_error` -- unexpected server error.
PublicApiError

Content

1 endpoint
get/api/v1/content/{pageKey}Get published page content

The tenant's published marketing content for a page, in the requested locale. Only the `home` page is available in v1. Draft content is never returned. If the tenant has no published content, `content` is `null`. Responses are cached for 60 seconds. Requires `Authorization: Bearer <Medoura API key>`. The key resolves to exactly one tenant -- no request names a tenant.

Parameters
pageKeyrequired
pathOnly `home` in v1. Any other value returns 404.
localeoptional
queryContent language. Defaults to `en`. Spanish content may be partial; sections not yet translated are omitted.
Responses
200The published page content, or `content: null` when none is published.
pageKey"home"required
locale"en" | "es"required
contentobjectnullrequired

Page sections keyed by section name, or null when nothing is published.

heroobjectoptional

Hero section: `badge`, `title`, `subtitle`.

howItWorksobjectoptional

How-it-works section: `badge`, `title`, `description`, and `steps` (`step1` to `step4`, each with `number`, `title`, `description`).

faqobjectoptional

FAQ section: `badge`, `title`, and `items` (each with `question` and `answer`).

testimonialsobjectoptional

Testimonials section: `badge`, `title`, and `items` (each with `name`, `beforeImageUrl`, `afterImageUrl`, optional `categories`).

401`invalid_key` -- the API key is missing, unknown, or revoked.
PublicApiError
403`feature_not_enabled` -- the Landing Page API is not enabled for this tenant. `account_locked` -- the account is locked or otherwise unavailable (for example for billing); contact support. A lock or unlock can take up to about a minute to take effect.
PublicApiError
404`not_found` -- `pageKey` is not `home`.
PublicApiError
422`validation_failed` -- `locale` is not `en` or `es`.
PublicApiError
429`rate_limited` -- 60 requests per minute and 5,000 per day per key. Includes a `Retry-After` header.
PublicApiError
500`internal_error` -- unexpected server error.
PublicApiError