joma.filmjoma

Developers

Manage your site, catalog and customer list from code or an AI assistant.

Get an API key

Sign in, open your organization, and go to Developer Access. Owners and admins can create keys.

joma API — Getting Started

The joma API lets you manage your organization from code or an AI tool: your site's pages, your films, series and merch, free-access grants, and your customer list.

  • Guides (this page and the two below) explain how to do things, with copy-paste examples.
  • Reference — every endpoint, field and error: joma.film/developers/reference. The machine-readable spec is at joma.film/openapi.json (OpenAPI 3.1). Point your AI tools or API client at it.

Base URL

https://joma.film/api/v1

Use this URL even if your site runs on your own domain. Your API key decides which organization you're working with, not the domain.

Authentication

Every request needs your organization's API key in the Authorization header:

curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/context

Get a key: sign in → your organization → Developer Access. Owners and admins can create keys. The key is shown once, so store it somewhere safe.

  • A key works for your whole organization, with the same access you have.
  • Create a separate key for each tool, so you can revoke one without affecting the others.
  • Treat a key like a password. If one leaks, revoke it in Developer Access right away.

Requests and responses

  • Send JSON with Content-Type: application/json. You get JSON back.
  • Money is always an integer number of cents (2000 = $20.00).
  • Timestamps are ISO 8601, in UTC.

Errors

Every error has the same shape:

{ "error": "title is required" }
Code Meaning
400 The request is invalid. The error message names the field and says why.
401 Missing, invalid or revoked API key. Check the Authorization header.
403 Allowed only in the joma dashboard, not with an API key (for example, making an episode free).
404 Not found, or it doesn't belong to your organization.
409 Conflicts with the current state, e.g. a slug that's taken or a page that changed since you read it.
413 Request body too large.
429 Rate limited. Wait and retry, backing off each time.
5xx Something went wrong on our side. Retry; if it persists, email support@joma.film.

Rate limits

Requests are rate limited. Normal use, like a daily sync job or an editing session, won't hit the limit. If you get a 429, back off and retry. Need more? Email support@joma.film.


Use it with an AI assistant (MCP)

joma runs a Model Context Protocol server, so an AI assistant can read and edit your site by chatting, using the same API underneath.

  • Server URL: https://joma.film/api/mcp
  • Auth: your API key as Authorization: Bearer YOUR_API_KEY.
  • Connecting an assistant that has no header field: open Developer Access → Edit with an AI assistant for the exact connection steps.

Use a dedicated key for each assistant so you can revoke it on its own.

Ask your assistant to call get_context first. It returns everything about your site, including the rules your content must follow.

Tools

Area Tools
Start here get_context
Pages list_pages, get_page, render_page, create_page, update_page, delete_page, publish_page, unpublish_page, discard_draft, list_versions, restore_version
Sections add_section, update_section, delete_section, reorder_sections
Site settings get_settings, update_settings, get_styles, update_styles, list_redirects, set_redirects, import_asset
Entities list_entities, create_entity, update_entity, delete_entity
Films list_films, get_film, update_film
Series list_series, create_series, update_series, create_season, add_episode, reorder_episodes
Merch list_merch, create_merch, update_merch, delete_merch

Edits made through the assistant stage as drafts. Nothing goes live until you, or your assistant with your go-ahead, publishes. Customer data and access grants are REST-only; the assistant can't read your customer list.

joma Content & Catalog API — Guide

For: filmmakers, curators and developers managing their joma site and catalog from code or an AI tool. New to the API? Start with Getting Started for the base URL, authentication, errors and rate limits. Every endpoint and field is in the reference.

All examples use https://joma.film/api/v1, which works whatever domain your site is on.


See everything available

Start here. One call returns your branding, pages, films, series, merch, entities, the section types you can use (with their exact content shapes), and the rules your content must follow.

curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/context

It's designed for AI tools. With an assistant, fetch this first and let it work from there.


Pages

Your site is made of pages. Each page has a slug (its URL path), sections (the content blocks) and settings (title, navigation, SEO).

Edits are drafts until you publish. Changing a page or its sections stages the change; visitors keep seeing the live version until you publish. You can preview the draft, discard it, or restore an earlier version at any time.

List and read

curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/content/pages
curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/content/pages/home

Reading a page returns the live page, any staged draft, and a version number (see Edit a section below).

Create a page

curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "About", "slug": "about"}' \
  https://joma.film/api/v1/content/pages

New pages start unpublished.

Update page settings

curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "About Us", "showInNav": true, "navOrder": 3, "metadata": {"seoDescription": "Who we are"}}' \
  https://joma.film/api/v1/content/pages/about

Send "slug": "new-slug" to rename a page.

Preview, publish, discard

# See the page as it will look, including unpublished edits (returns HTML)
curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/content/pages/about/render

# Publish the draft
curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"isPublished": true}' https://joma.film/api/v1/content/pages/about

# Unpublish
curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"isPublished": false}' https://joma.film/api/v1/content/pages/about

# Throw away unpublished edits
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/content/pages/about/discard

Publishing checks every section first. If one can't go live (for example, it shows a film that isn't available), you get a 400 naming the section.

Versions and restore

Every edit, publish and delete saves a version.

# Versions of one page, newest first
curl -H "Authorization: Bearer YOUR_API_KEY" "https://joma.film/api/v1/content/versions?slug=home"

# Restore one (the current state is saved as a version first)
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
  https://joma.film/api/v1/content/versions/VERSION_ID/restore

A deleted page can be restored the same way.

Delete a page

curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/content/pages/about

Sections

Each page is a list of sections. A section is either structured (you provide the data; joma renders it in your brand) or HTML (you write the markup).

Structured sections (preferred)

{
  "type": "text_block",
  "content": { "heading": "Now streaming", "body": "Watch the film anywhere.", "align": "center" }
}

Structured sections use your brand colors and fonts automatically, stay consistent across your site, and can show live catalog data (titles, posters, prices) by referring to a film instead of copying its details.

Type What it shows
composition A flexible layout you build from text, images, quotes, buttons and film actions. The most versatile structured type.
hero Full-width background image or video with a title and buttons.
text_block Heading and body text, with optional buttons.
film_card One film: poster, title, price, and buy/rent buttons.
merch_grid Your merch, as a grid or carousel.
press_quotes Quotes from your press_quote entities.
press_article_list Press coverage as article rows (quote, outlet logo, link).
events Screenings from your screening entities.
contact_form Name / email / message form that emails you.
form A form with the fields you define; submissions are emailed to you.
newsletter_signup Email signup for the newsletter provider set in your settings.
earnings_calculator “Become a curator” calculator for one of your films.

The exact content shape for each type is in GET /context → sectionTypes[].contentShape.

Prices are never typed into content. Sections refer to a film by slug and joma shows its current price, so a literal price in a section is rejected.

HTML sections

{ "type": "html", "content": "<div class='my-banner'><h2>Your content here</h2></div>" }

Use HTML when no structured type fits. Style it with section styles or your site-wide CSS (see Site styles). Scripts are removed unless joma has enabled them for your organization.

Add a section

curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "text_block", "content": {"heading": "Screenings", "body": "Coming to a city near you."}}' \
  https://joma.film/api/v1/content/pages/home/sections

Add "index": 3 to insert at a position (the default is the end). Add "name" to label it for editors.

Edit a section

Sections are addressed by position (0-based). Pass the page version you last read as expectedVersion. If someone else changed the page since then you get a 409 instead of overwriting their work.

curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": {"heading": "New heading", "body": "New text"}, "expectedVersion": 12}' \
  https://joma.film/api/v1/content/pages/home/sections/1

Remove and reorder

# Remove the section at position 2
curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" \
  https://joma.film/api/v1/content/pages/home/sections/2

# New order: section 2 first, then 0, then 1
curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"order": [2, 0, 1]}' https://joma.film/api/v1/content/pages/home/sections/reorder

Site settings and branding

curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/content/settings

curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"primaryColor": "#b90000", "logoUrl": "https://example.com/logo.png", "description": "Independent documentaries from the Pacific Northwest"}' \
  https://joma.film/api/v1/content/settings

You can set your name, a short description, a longer bio, your logo, brand colors, layout, and customData. customData is merged into what's there, not replaced:

curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"customData": {"contactEmail": "info@example.com", "socialLinks": {"instagram": "https://instagram.com/myfilm"}}}' \
  https://joma.film/api/v1/content/settings

Your navigation, text styles, surface colors and newsletter provider also live in customData. Their shapes and current values are in GET /context → customization. If any field in a settings update is invalid, nothing is saved.

Site styles

CSS that applies to every page, plus optional head and footer scripts.

curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/content/styles

curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"globalStyles": ".my-banner { padding: 3rem; }"}' \
  https://joma.film/api/v1/content/styles

This affects your whole site, so change it carefully. CSS is sanitized, and scripts only run where joma has enabled them for your organization.

Redirects

Send old URLs on your site to new ones. PUT replaces the whole list (send [] to clear).

curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"redirects": [{"from": "/old-trailer", "to": "/films/my-film"}, {"from": "/promo", "to": "/about", "permanent": false}]}' \
  https://joma.film/api/v1/content/redirects

Redirects are permanent (308) unless you set "permanent": false (307). Paths must start with /.

Hosted images

Copy an image from another host into joma-hosted storage, so your pages don't depend on a link that might break (for example, when leaving a store platform).

curl -X POST -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"sourceUrl": "https://cdn.example.com/poster.jpg"}' \
  https://joma.film/api/v1/content/assets
# → { "url": "https://…" }  use this URL in your sections

Entities (screenings, press, team…)

Structured items your sections display: screening, press_quote, press_outlet, podcast_episode, team_member. New types are added by joma on request.

# List (published only by default; add &publishedOnly=false for all)
curl -H "Authorization: Bearer YOUR_API_KEY" "https://joma.film/api/v1/content/entities?type=screening"

# Create
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"entityType": "screening", "data": {"title": "Portland Premiere", "venue": "Hollywood Theatre", "city": "Portland", "region": "OR", "date": "2026-06-15", "ticketUrl": "https://tickets.example.com", "status": "upcoming"}}' \
  https://joma.film/api/v1/content/entities

# Update: `data` REPLACES the entity's data, so send the whole object
curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"data": {"title": "Portland Premiere", "venue": "Hollywood Theatre", "date": "2026-06-15", "status": "past"}}' \
  https://joma.film/api/v1/content/entities/ENTITY_ID

# Delete
curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/content/entities/ENTITY_ID

Films

curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/catalog/films
curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/catalog/films/my-film

Reading one film includes its reviews and editablePageKnobs, the page settings you may change on that film. Prices and availability are shown but can only be changed in the dashboard.

Update a film (PATCH)

Send only what you want to change:

curl -X PATCH -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "shortDescription": "A road movie about home.",
    "featuredReviewIds": ["REVIEW_ID_1", "REVIEW_ID_2"],
    "showTrailer": true,
    "displaySettings": { "pageLayout": "hero", "heroTheme": "dark" }
  }' \
  https://joma.film/api/v1/catalog/films/my-film
Field What it does
shortDescription, longDescription The hook and the synopsis.
filmmakerBio “About the filmmaker” text for this film (up to 2000 characters). null uses your organization's bio.
featuredReviewIds Which of the film's reviews to feature, in order. [] clears.
showTrailer, showReviews, showFilmmakerBio Show or hide those parts of the page.
displaySettings How the page looks (below).
seasonBundleId, episodeNumber, episodeAccess Place the film in a season (see Series). free is set in the dashboard.

Any other field returns 400. Pricing, availability, visibility and video are managed in the dashboard.

Film page settings (displaySettings)

displaySettings is merged: send only the keys you're changing. Send a key as null to clear it.

Available on every film:

Key Values
pageLayout auto, hero, poster
heroTheme auto, dark, light
titleColor A hex color like #dc2626

Available on films where joma has set up a custom page (they appear in editablePageKnobs):

Key Values
presenterBadge Short text chip above the title (up to 80 characters).
supertitle Small line above the title, e.g. a production credit (up to 120 characters).
hidePresentedBy true hides the “Presented by …” line.
aboutLayout centered or image-text-quote (three columns: image, about text, featured review).
aboutImageUrl Image for the three-column about layout.

To add one of these to a film, contact joma.


Series

A series has seasons; each season has episodes, and each episode is one of your films.

# Everything: series → seasons → episodes
curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/catalog/series

# Create a series, then a season
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"title": "Ready When You Are", "description": "A six-part series."}' \
  https://joma.film/api/v1/catalog/series
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"seasonTitle": "Season One"}' \
  https://joma.film/api/v1/catalog/series/ready-when-you-are/seasons

# Add one of your films as an episode
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"filmSlug": "my-film", "episodeNumber": 1}' \
  https://joma.film/api/v1/catalog/series/ready-when-you-are/seasons/1/episodes

# Reorder episodes (every episode's film slug, once, in the new order)
curl -X PUT -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"order": ["episode-two", "my-film"]}' \
  https://joma.film/api/v1/catalog/series/ready-when-you-are/seasons/1/episodes

episodeAccess sets how viewers reach an episode: included (comes with the season, the default) or paid (sold on its own too). Making an episode free, so anyone signed in can watch it, is a pricing change: do it in the dashboard (the API returns 403). New seasons start off sale; set the season's price and put it on sale in the dashboard. The response includes the link. Update a series with PATCH /catalog/series/{slug}. You can also read a single series, its seasons, or one season's episodes.


Merch

# List (active only; add ?includeInactive=true for all)
curl -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/catalog/merch

# Create (price in cents)
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"name": "Poster", "price": 2500, "imageUrl": "https://…", "options": {"Size": ["18x24", "24x36"]}, "requiresShipping": true}' \
  https://joma.film/api/v1/catalog/merch

# Update (send only what changes) and delete
curl -X PATCH -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"price": 2000, "stockQuantity": 40}' https://joma.film/api/v1/catalog/merch/ITEM_ID
curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/catalog/merch/ITEM_ID

stockQuantity: null means unlimited. Deleting an item that's already on an order or in a bundle deactivates it instead (softDeleted: true), so order history stays intact. To hide an item without deleting it, set isActive: false.


Free access (grants)

Give someone free access to one of your films, for example press, festival programmers or crew. joma emails them a link to claim it.

# Grant access (permanent unless you set expiresAt)
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"email": "critic@example.com", "filmSlug": "my-film", "expiresAt": "2026-12-31T23:59:59Z", "note": "Thanks for covering the festival!"}' \
  https://joma.film/api/v1/access/grants

# List a film's grants (filter with &status=pending|accepted|revoked|declined)
curl -H "Authorization: Bearer YOUR_API_KEY" "https://joma.film/api/v1/access/grants?filmSlug=my-film"

# Revoke a grant that hasn't been claimed yet
curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" https://joma.film/api/v1/access/grants/GRANT_ID

Set "sendEmail": false to send the claim link yourself (it's in the response as claimUrl). Granting the same email again refreshes the invite. Once someone claims access it's in their library and can't be revoked through the API. A daily limit applies.


Tips

Keep your content in git. Fetch pages as JSON and commit them for your own history. joma also keeps versions you can restore (see Versions and restore).

Use AI tools. Point your assistant at GET /context or connect it through MCP (see Getting Started). It can draft sections, preview with render, and publish when you say so.

The platform handles the hard parts. Checkout, streaming, accounts, curator links and payouts are run by joma, and nothing in your pages can break them.


What you can't change via the API

Managed in the joma dashboard, or by joma support:

  • Film pricing, availability, visibility and video files
  • Creating films; season prices and putting seasons on sale
  • Curator program settings and payouts
  • Payment (Stripe) setup
  • Domains and DNS
  • User accounts and team members

joma Customer API

For: filmmakers and curators who want to sync their joma customers into a mailing tool (Klaviyo, Mailchimp, etc.). New to the API? Start with Getting Started for authentication, errors and rate limits.


Why this exists

You can download your customer list as a CSV from your dashboard, including a Marketing Opt-Out column. But a one-time import goes stale: if a customer opts out next week, your mailing tool never finds out.

The Customer API lets you re-sync on a schedule (daily is recommended), so every opt-out reaches your mailer. Same data as the CSV, machine-readable and always current.


Authentication

Send your organization's API key as Authorization: Bearer YOUR_API_KEY (see Getting Started). If your organization is both a filmmaker and a curator, the same key works for both endpoints; each returns its own list.

These endpoints return your customers' email addresses. Keep the key on your server, never in a browser or a public repo.


Endpoints

GET /api/v1/customers/filmmaker

Customers who bought a film your organization owns.

Response:

{
  "customers": [
    {
      "email": "fan@example.com",
      "firstPurchase": "2026-04-12T18:33:21.000Z",
      "lastPurchase": "2026-05-01T10:11:02.000Z",
      "totalPurchases": 3,
      "totalSpentCents": 4500,
      "marketingOptOut": false
    }
  ],
  "total": 1,
  "fetchedAt": "2026-05-01T22:31:18.512Z"
}

GET /api/v1/customers/curator

Customers who bought through your curator link or code.

Response:

{
  "customers": [
    {
      "email": "fan@example.com",
      "firstPurchase": "2026-04-12T18:33:21.000Z",
      "lastPurchase": "2026-05-01T10:11:02.000Z",
      "filmsPurchased": ["The Long Walk", "I Don't Know Jack"],
      "totalPurchases": 2,
      "totalSpentCents": 3500,
      "yourEarningsCents": 1050,
      "marketingOptOut": false
    }
  ],
  "total": 1,
  "fetchedAt": "2026-05-01T22:31:18.512Z"
}

Field reference

Field Type Notes
email string Customer's account email — primary key for syncing into a mailer.
firstPurchase ISO 8601 string (UTC) Earliest completed purchase.
lastPurchase ISO 8601 string (UTC) Most recent completed purchase.
totalPurchases number Number of completed purchases from you.
totalSpentCents number Total charged, in cents.
marketingOptOut boolean true means the customer opted out of marketing. Per the joma terms you must not include them in marketing campaigns. Transactional messaging about their purchase is still OK.
filmsPurchased string[] (Curator only.) Titles the customer bought through your link or code.
yourEarningsCents number (Curator only.) Your earnings from this customer's purchases, in cents.

Recommended sync pattern

// Pseudocode for a daily Klaviyo sync
const res = await fetch("https://joma.film/api/v1/customers/filmmaker", {
  headers: { Authorization: `Bearer ${process.env.JOMA_API_KEY}` },
});
const { customers } = await res.json();

for (const c of customers) {
  await klaviyo.upsertProfile({
    email: c.email,
    properties: {
      joma_first_purchase: c.firstPurchase,
      joma_last_purchase: c.lastPurchase,
      joma_total_spent: c.totalSpentCents / 100,
    },
    // CRITICAL — translate joma's flag to your mailer's consent model.
    consentMarketing: !c.marketingOptOut,
  });
}

Recommendation: sync at least daily. Always overwrite consent based on the latest marketingOptOut value — never let a stale local cache override it.


Errors and rate limits

See Getting Started. A daily sync job won't hit the rate limit; on a 429, back off and retry.