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.