Run a Mosana business from code or an assistant, and build a website that sells and shows what a profile has. Payment, watching, learning and billing always finish on Mosana, so a site you build never handles money or media.
There are four ways in. Pick by what you are building:
| Your own scripts | A personal key, made at Developers, can do only what you tick, on the profiles you are on. REST under https://mosana.chaldeastudios.com/api/v1. |
| An assistant | Claude, ChatGPT and other assistants connect to the MCP server and sign in with Mosana. They run your Mosana account, and can build you a website. |
| A website | One website key (made in Sell → Website) bound to one profile. It reads the profile and starts payments; nothing else. |
| Your customers | Sign in with Mosana on your website. The site sees only that customer's orders and downloads from your profile. |
A first call, with a key:
curl https://mosana.chaldeastudios.com/api/v1/me \ -H "Authorization: Bearer msk_…"
The same operations are the MCP tools and the REST endpoints below, with one set of inputs and permissions.
Every call carries a token as Authorization: Bearer ….
| profile:read | See your account, your business profiles and their posts |
| profile:write | Change your account's and your profiles' details and pictures |
| products:read | See your profiles' products, drafts, membership plans and comments |
| products:write | Create and edit products (as drafts), pictures, files and videos |
| products:publish | Put products on sale, take them off, or archive them |
| orders:read | See your profiles' orders |
| orders:write | Mark orders ready or handed over |
| posts:write | Write, edit and delete posts as your profiles |
| comments:write | Reply to comments |
| notifications:read | See your notifications |
| checkout:create | Make payment links for your profiles' products (the shopper pays on Mosana's own page) |
Money (payouts, earnings, commission, refunds, plan billing) is not in the API: the account holder manages it in Mosana.
The owner makes the website in Mosana: Sell → (profile) → Websites → Connect a website. They enter the website's addresses and get one key. The website's only setting is that key:
MOSANA_KEY=msk_… # server only MOSANA_URL=https://mosana.chaldeastudios.com # optional
GET https://mosana.chaldeastudios.com/api/v1/site returns the profile, the registered addresses and the id customers sign in with, so nothing else is configured. The website works out its own address from each request, and refuses to start payments from an address that is not registered.
The owner's name for the website is only for their records. Customers see the profile's name and logo.
Show everything the profile has, in whatever state it is set in Mosana, and hand over the rest:
| Paid goods, downloads, books, courses, films | Cart, then a payment link. Afterwards the customer watches, reads or learns on Mosana. |
| Free (free true) | A “Free” tag and a button to the item's url on Mosana. |
| Members-only video (plan_id) | Tag it “Members”; its page says to join the membership to watch it, with the plan's join_url. |
| Membership | Price and what it includes, and a button to join_url. |
How it is shown is yours: sections, a featured film, collections and copy around the listings. The example website, with working code for all of this, is github.com/chaldeastudios/mosana-test-ecommerce.
Building with an assistant? Connect it to the MCP server and ask it to read the guide: the resource mosana://guides/build-a-website, the tool get_website_guide, or /docs/build-a-website.md as plain text.
To keep the customer on your site, open a tab from the click itself (browsers block it otherwise), create the link, then set the tab to url. While it is open, ask your own server (which asks GET /pay-links/{id}) every few seconds, and when the customer returns to your tab. If the tab is blocked, redirect instead.
Mosana also tells your server by itself, with nothing to set up. When a pay link is paid, or runs out unpaid, it posts an event to <the origin of return_url>/api/mosana/webhook:
{ "id": "evt_<pay_link>_paid", "type": "pay_link.paid", "created": 1761390000,
"data": { "pay_link": "…", "status": "paid", "profile_id": "…", "return_url": "…",
"paid_at": "2026-10-25T11:23:40+00:00", "orders": ["MOS-…"], "saved_to_cart": false } }The header x-mosana-signature: t=<seconds>,v1=<hex> is the HMAC-SHA256 of "<t>.<body>", keyed with the text of the SHA-256 hex of your website's key, which you work out yourself from it. Refuse an event more than five minutes old. Answer 2xx. A failure is retried over about a day; a 404 means “I don't listen” and ends it. An event may arrive twice, so its id is its identity. Use it to record the order or send your own email even if the customer closed the page.
Unpaid links are tidied up: deleted after 7 days for a guest (or once the items are in a cart), after 30 days otherwise; paid ones after 180 days. The orders themselves are never deleted.
OAuth authorization code with PKCE (S256), as a public client, using the website's client_id from /site. The customer sees “Sign in to <profile> with Mosana” with the profile's logo, and the site receives only these permissions:
| shopper:profile | Know who you are: your name and email |
| shopper:orders | See your orders from this shop only |
| shopper:library | See and download what you bought from this shop |
Add Mosana to an assistant as a custom MCP connector with this address, then sign in with Mosana and choose what it may do:
https://mosana.chaldeastudios.com/mcp
The assistant sees the tools below, limited to what it was allowed, and one resource, the website guide. Manage connected apps and keys at Developers.
Each is a REST endpoint under https://mosana.chaldeastudios.com/api/v1 and an MCP tool of the same name.
Who you are signed in as on Mosana, and the business profiles you are on (with each profile's id, your role, its status and plan). Start here: every profile tool needs a profile_id from this list.
Needs profile:read.
Change your own name, phone number or profile picture (an image_id from add_image_from_url or upload_image with purpose avatar; an empty string removes it).
Needs profile:write.
| name | string | Your name (2 to 80 characters). |
| phone | string | Your phone number. |
| avatar_id | string | Picture id for your avatar (image_id with purpose avatar), or an empty string to remove it. |
A business profile's details as its page shows them (name, bio, location, pictures, status, plan) and how many people follow it.
Needs profile:read.
| profile_id * | string | The business profile's id (from get_account). |
Change a business profile's details: its name, bio, location, logo or cover picture (image_ids from add_image_from_url or upload_image; an empty string removes one), and the pickup address and phone for physical orders. Owners only. Only what is sent changes.
Needs profile:write · the profile's owner.
| profile_id * | string | The business profile's id (from get_account). |
| name | string | The profile's name (2 to 80 characters). |
| bio | string | About the profile (up to 1000 characters). |
| location | string | Where it is (up to 120 characters). |
| logo_id | string | Picture id for the logo (purpose logo). |
| cover_id | string | Picture id for the cover (purpose cover). |
| pickup_address | string | Where couriers collect physical orders. |
| pickup_phone | string | Phone for pickups. |
The profile's products (physical items, downloads, books, courses, videos), newest change first, drafts included, with a line about each (get_product has the full text). Filter by status, kind or words; page with limit and offset.
Needs products:read.
| profile_id * | string | The business profile's id (from get_account). |
| status | string | Only this status. One of: draft, active, hidden, archived. |
| kind | string | Only this kind. One of: physical, digital, book, course, video. |
| q | string | Only products matching these words. |
| limit | integer | How many (1 to 200, default 50). |
| offset | integer | Skip this many (for the next page). |
One of the profile's products in full (its options and their variants, pictures, video, file), with its page link.
Needs products:read.
| profile_id * | string | The business profile's id (from get_account). |
| product_id * | string | The product's id. |
Create a product on the profile, always as a draft (nobody sees it until publish_product). Kinds: physical (delivered by the profile), digital (a download), book (an ebook), course, video. A download's or book's file is added with create_file_upload or add_file_from_url; a video's film with video_id (from create_video_upload or add_video_from_url).
Needs products:write.
| profile_id * | string | The business profile's id (from get_account). |
| kind * | string | What it is. One of: physical, digital, book, course, video. |
| name * | string | The product's name (up to 160 characters). |
| description | string | What it is, for the product page (up to 8000 characters). |
| access | string | How people get it: paid (bought at checkout at price_minor), members (included in the membership plan plan_id: the plan is the price) or free (free to everyone). Nothing is free unless it says free; a price of 0 does not make it free. A listing made without access is paid for, so it needs a price to go on sale. Only videos and courses can be for members; a physical item is always paid. One of: paid, members, free. |
| price_minor | integer | Price in cents: KES 250 is 25000. Used when access is paid; members and free listings have none. |
| max_qty | integer | Most a buyer can take in one order (1 to 99). Books, downloads, courses and videos are bought once. |
| gallery | array | Picture ids (image_id from add_image_from_url or upload_image, with purpose product), in order. Up to 12. |
| attrs | object | Details shown on the page, such as author, pages, language, size or material: short keys (a-z, _) to short text. |
| categories | array | Category slugs or ids the product belongs to. |
| stock_qty | integer | Physical items: how many are in stock. Leave out not to count stock. |
| weight_g | integer | Physical items: weight in grams, for delivery. |
| plan_id | string | Videos and courses: the membership plan that includes it, when access is members (from list_plans). |
| video_id | string | Videos: the film (video_id from create_video_upload or add_video_from_url). |
| trailer_id | string | Movies: the trailer's video_id. |
| video_type | string | Videos: a plain video, a movie (its own page with a trailer) or an episode of a series. One of: video, movie, episode. |
| director | string | Movies: who made it. |
| cast | string | Movies: who is in it. |
| captions | string | Movies: caption languages. |
| rating | string | Movies: age rating. One of: G, PG, 13, 16, 18. |
| year | integer | Movies: release year. |
| chapters | string | Videos: chapters, one per line, a time then a title, e.g. "0:00 Welcome". Empty clears them. |
| series | string | Episodes: the series' name, the same on each of its episodes. |
| episode | integer | Episodes: its number in the series. |
| released | string | Episodes: release date, YYYY-MM-DD. |
Change a product's details. Only what is sent changes (attrs are merged into what it has; a null or empty value removes one); whether it is on sale stays as it is (that is publish_product's job).
Needs products:write.
| profile_id * | string | The business profile's id (from get_account). |
| product_id * | string | The product's id. |
| name | string | The product's name (up to 160 characters). |
| description | string | What it is, for the product page (up to 8000 characters). |
| access | string | How people get it: paid (bought at checkout at price_minor), members (included in the membership plan plan_id: the plan is the price) or free (free to everyone). Nothing is free unless it says free; a price of 0 does not make it free. A listing made without access is paid for, so it needs a price to go on sale. Only videos and courses can be for members; a physical item is always paid. One of: paid, members, free. |
| price_minor | integer | Price in cents: KES 250 is 25000. Used when access is paid; members and free listings have none. |
| max_qty | integer | Most a buyer can take in one order (1 to 99). Books, downloads, courses and videos are bought once. |
| gallery | array | Picture ids (image_id from add_image_from_url or upload_image, with purpose product), in order. Up to 12. |
| attrs | object | Details shown on the page, such as author, pages, language, size or material: short keys (a-z, _) to short text. |
| categories | array | Category slugs or ids the product belongs to. |
| stock_qty | integer | Physical items: how many are in stock. Leave out not to count stock. |
| weight_g | integer | Physical items: weight in grams, for delivery. |
| plan_id | string | Videos and courses: the membership plan that includes it, when access is members (from list_plans). |
| video_id | string | Videos: the film (video_id from create_video_upload or add_video_from_url). |
| trailer_id | string | Movies: the trailer's video_id. |
| video_type | string | Videos: a plain video, a movie (its own page with a trailer) or an episode of a series. One of: video, movie, episode. |
| director | string | Movies: who made it. |
| cast | string | Movies: who is in it. |
| captions | string | Movies: caption languages. |
| rating | string | Movies: age rating. One of: G, PG, 13, 16, 18. |
| year | integer | Movies: release year. |
| chapters | string | Videos: chapters, one per line, a time then a title, e.g. "0:00 Welcome". Empty clears them. |
| series | string | Episodes: the series' name, the same on each of its episodes. |
| episode | integer | Episodes: its number in the series. |
| released | string | Episodes: release date, YYYY-MM-DD. |
Put a product on sale (status active), take it off sale (draft or hidden) or archive it. The profile must be approved; it needs a picture, downloads and books their file, videos their film, and courses a lesson.
Needs products:publish.
| profile_id * | string | The business profile's id (from get_account). |
| product_id * | string | The product's id. |
| status | string | The new status (default active). One of: active, draft, hidden, archived. |
The profile's membership plans (paid access to its videos, billed monthly, quarterly or yearly), with price and how many members each has.
Needs products:read.
| profile_id * | string | The business profile's id (from get_account). |
Make a membership plan (name, description, price in cents and how often it is billed), or rename or redescribe one (give plan_id). Only the profile's owner can. Change an existing plan's price with change_plan_price, which tells members first. Videos, courses, books and downloads with access members are included in a plan.
Needs products:write.
| profile_id * | string | The business profile's id (from get_account). |
| plan_id | string | A plan to change; leave out to make one. |
| name | string | The plan's name. |
| description | string | What members get. |
| amount_minor | integer | The price in cents (KES 500 is 50000), for a new plan. |
| interval | string | How often it is billed, for a new plan. One of: monthly, quarterly, annually. |
Schedule a new price for a plan from a date. Members are told before it changes; a rise needs their agreement.
Needs products:write.
| plan_id * | string | The plan's id. |
| amount_minor * | integer | The new price in cents. |
| description | string | What changes for members. |
| effective_at | string | When it takes effect (YYYY-MM-DD). |
Bring a picture into Mosana from a public image URL (jpg, png or webp) and get its image_id, to use in a product's gallery, a post's media, a profile's logo or cover, or your avatar. Pass profile_id for every purpose except avatar.
Needs products:write or posts:write or profile:write.
| url * | string | A public https link to the image. |
| purpose * | string | What it is for. One of: product, post, logo, cover, avatar. |
| profile_id | string | The business profile's id (from get_account). |
Bring a picture into Mosana from the file itself, sent as base64 (jpg, png or webp, up to 3 MB), and get its image_id. Use add_image_from_url instead when the picture is at a public link.
Needs products:write or posts:write or profile:write.
| data * | string | The picture's bytes as base64 (a data: URI also works). |
| purpose * | string | What it is for. One of: product, post, logo, cover, avatar. |
| profile_id | string | The business profile's id (from get_account). |
A profile's pictures (by purpose: product, post, logo or cover), newest first, with their image_id, link and how many places use each.
Needs products:read or profile:read.
| purpose * | string | Which pictures. One of: product, post, logo, cover. |
| profile_id * | string | The business profile's id (from get_account). |
| limit | integer | How many (1 to 100, default 50). |
| offset | integer | Skip this many (for the next page). |
Delete one of a profile's pictures (or one of your avatars) for good. Take it out of any gallery or post first.
Needs products:write or posts:write or profile:write.
| image_id * | string | The picture's id. |
A one-time link to upload the file buyers receive for a download or a book (PDF): PUT the bytes to upload_url, then call attach_file with the file_id. Use add_file_from_url instead when the file is at a public link.
Needs products:write.
| profile_id * | string | The business profile's id (from get_account). |
| product_id * | string | The download's or book's product id. |
| filename * | string | The file's name, e.g. guide.pdf. |
Make an uploaded file (from create_file_upload) the one buyers of a download or book receive. The previous file is no longer given out.
Needs products:write.
| profile_id * | string | The business profile's id (from get_account). |
| product_id * | string | The product's id. |
| file_id * | string | The file_id create_file_upload returned. |
| filename | string | The file's name as buyers see it (default: as given when the upload was made). |
Copy the file buyers receive (a download or a book's PDF, up to 250 MB; bigger files go through create_file_upload) from a public link into Mosana's storage and attach it to the product in one step.
Needs products:write.
| profile_id * | string | The business profile's id (from get_account). |
| product_id * | string | The product's id. |
| url * | string | A public https link to the file. |
| filename | string | The file's name as buyers see it (default: from the link). |
A five-minute link to the file buyers of a download or book receive, to check it is the right one. (Buyers read a book in Mosana's reader: only the profile's team can download a book's PDF.)
Needs products:read.
| profile_id * | string | The business profile's id (from get_account). |
| product_id * | string | The product's id. |
A one-time link to upload a video file for the profile. The video is private. PUT the file to upload_url, then call get_video with the video_id until its status is ready, and set it on a video product (video_id or trailer_id).
Needs products:write.
| profile_id * | string | The business profile's id (from get_account). |
| purpose | string | What it is for (default film). One of: film, trailer, lesson, post. |
| filename | string | The file's name. |
Have Mosana fetch a video from a public link for the profile. The video is private. Call get_video with the video_id until its status is ready.
Needs products:write.
| profile_id * | string | The business profile's id (from get_account). |
| url * | string | A public https link to the video file. |
| purpose | string | What it is for (default film). One of: film, trailer, lesson, post. |
Where one of the profile's videos stands (waiting_for_file, preparing, ready, errored) and, once ready, its length and a still from it.
Needs products:read.
| profile_id * | string | The business profile's id (from get_account). |
| video_id * | string | The video's id, from create_video_upload, add_video_from_url or list_videos. |
The profile's videos, newest first, with their status.
Needs products:read.
| profile_id * | string | The business profile's id (from get_account). |
| limit | integer | How many (1 to 100, default 50). |
| offset | integer | Skip this many (for the next page). |
Delete one of the profile's videos for good.
Needs products:write.
| profile_id * | string | The business profile's id (from get_account). |
| video_id * | string | The video's id. |
A one-time link to upload a file lesson's file (a worksheet, slides). PUT the bytes to upload_url, then call attach_lesson_file with the path. The file stays private: only the people taking the course (or anyone, for a preview lesson) can download it.
Needs products:write.
| lesson_id * | string | The lesson's id. |
| filename * | string | The file's name. |
Make the file sent to create_lesson_file_upload's address the lesson's file (the lesson becomes a file lesson).
Needs products:write.
| lesson_id * | string | The lesson's id. |
| path * | string | The path create_lesson_file_upload returned. |
| filename | string | The name buyers see. |
The profile's paid orders, newest first: status, amounts (subtotal and delivery), the lines bought and delivery details. Money (payouts, earnings, commission, refunds) is in the Mosana app.
Needs orders:read.
| profile_id * | string | The business profile's id (from get_account). |
| status | string | Only orders in this status. One of: placed, ready, handed_over, completed. |
| limit | integer | How many (1 to 200, default 50). |
One of the profile's orders, by its id or its order number (MOS-XXXX-XXXX).
Needs orders:read.
| profile_id * | string | The business profile's id (from get_account). |
| order_id * | string | The order's id or number. |
Move a paid order on: ready (packed), handed_over (given to the courier or collected) or completed. It only moves forward. Cancelling an order refunds the buyer, so that is done in the Mosana app.
Needs orders:write.
| profile_id * | string | The business profile's id (from get_account). |
| order_id * | string | The order's id or number. |
| status * | string | The new status. One of: ready, handed_over, completed. |
The profile's posts, newest first, with likes, comments and saves.
Needs profile:read.
| profile_id * | string | The business profile's id (from get_account). |
| limit | integer | How many (1 to 50, default 20). |
| before | string | Only posts before this time (ISO 8601), for the next page. |
Post as the profile to its followers' feeds: text, up to 10 pictures (image_ids from add_image_from_url or upload_image, with purpose post), and optionally one of its products to feature.
Needs posts:write.
| profile_id * | string | The business profile's id (from get_account). |
| body | string | The post's text. |
| media | array | Picture ids, up to 10. |
| product_id | string | A product to feature. |
Change the text (or the pictures) of one of your profiles' posts.
Needs posts:write.
| post_id * | string | The post's id. |
| body | string | The new text. |
| media | array | The pictures, replacing the ones it has. |
Take one of your profiles' posts down (it disappears from feeds and the profile).
Needs posts:write.
| post_id * | string | The post's id. |
The comments on a product or a post, with likes, replies (parent_id) and whether the commenter bought the product.
Needs products:read.
| product_id | string | A product's id. |
| post_id | string | Or a post's id. |
Comment on a product or post, or reply to a comment (send parent_id). It is written as you, the account holder.
Needs comments:write.
| product_id | string | A product's id. |
| post_id | string | Or a post's id. |
| parent_id | string | The comment being answered. |
| body * | string | The reply. |
What is new for the account: replies and likes on its comments, posts from profiles it follows, its orders moving on, and for its profiles new orders, comments and followers.
Needs notifications:read.
| since | string | Only what came after this time (ISO 8601). |
For a website's own key (made in Mosana: Sell → Websites): which profile it sells for, its name and domains, and the client_id customers sign in with. A website needs nothing else configured: it asks this once and caches it.
Needs profile:read.
Make a basket for a shopper to pay for, and get its url: send the shopper there. They pay on Mosana's own page (card, M-Pesa or a saved card; delivery address for physical items) and come back to return_url. Prices, delivery, fees and stock are worked out by Mosana at that moment, never taken from the shop. The items must be this profile's products on sale (a product with options needs the variant's id). return_url must be a page on one of the website's registered domains (Sell → Websites in Mosana). If the shopper leaves without paying, the items are put in their Mosana cart. Check the result with get_pay_link: never trust the return address alone.
Needs checkout:create.
| profile_id * | string | The business profile's id (from get_account). |
| items * | array | What to sell: [{ "product_id": "...", "qty": 1 }]. For a product with options use the variant's id as product_id. |
| return_url * | string | Where the shopper goes after paying or leaving: a page on one of the shop's addresses. Mosana adds ?pay_link=&status=&order= to it. |
| string | The shopper's email, if the shop knows it (for the receipt and for saving an abandoned basket). | |
| minutes | integer | How long the link stays open (5 to 180, default 30). |
Whether a payment link was paid: status open, paid or expired; when paid, the order numbers it made (use get_order for each). expired links that were never paid have their items saved in the shopper's Mosana cart (saved_to_cart).
Needs checkout:create.
| profile_id * | string | The business profile's id (from get_account). |
| pay_link_id * | string | The payment link's id. |
Who is signed in to this shop with Mosana, and which shop and profile it is. Name and email only if the customer allowed shopper:profile.
Needs shopper:profile or shopper:orders or shopper:library.
This customer's paid orders from this shop's profile only, newest first. Their orders elsewhere on Mosana are never shown; more_url on each leads to the full order in Mosana.
Needs shopper:orders.
| limit | integer | How many (1 to 100, default 30). |
| before | string | Only orders before this time (ISO), to page. |
One of this customer's orders from this shop's profile, by its number (MOS-XXXX-XXXX).
Needs shopper:orders.
| order_number * | string | The order number, MOS-XXXX-XXXX. |
Downloads, books, courses and videos this customer owns from this shop's profile. A file can be fetched with shopper_download; courses, books and videos open in Mosana (open_url).
Needs shopper:library.
A five-minute link to the file of a download or book this customer bought from this shop's profile.
Needs shopper:library.
| product_id * | string | The product's id (from shopper_library). |
The guide for building someone's own website (shop, film site, course site...) on Mosana with a single website key: what to read, how to show free, paid and members-only things, how to take payment with pay links, how customers sign in, and how to curate sections such as a featured film. Read this first when asked to build or change a website on Mosana; the example website is github.com/chaldeastudios/mosana-test-ecommerce.
Needs profile:read.
* required.
An error's body is {"error": {"status", "code", "message", "request_id"}}. A token may make 120 calls a minute; over that, wait for the Retry-After seconds.
| 200 | ok | Done. The body is the result. |
| 400 | bad_request, invalid_json | The call is malformed: a missing or wrong argument, or a body that isn't JSON. The message names it. |
| 401 | unauthorized | No token, or one that is unknown, revoked or expired. Make a new key, or reconnect the app. |
| 403 | missing_scope, forbidden | The token lacks the permission, or the account may not do this (not its profile, or only the owner may). |
| 404 | not_found | No such endpoint, or nothing with that id this account can see. |
| 405 | method_not_allowed | The path exists with another method; the Allow header lists them. |
| 409 | conflict | A rule stops it now: publishing without a file, an order that can't move that way. |
| 413 | too_large | The file is over the limit (250 MB). |
| 422 | rejected | Understood, but a rule refused it (a post with nothing in it). |
| 429 | rate_limited | Over 120 calls a minute for this token. Wait for Retry-After seconds. |
| 500 | internal | Mosana's fault. Quote the request id. |
| 502 | upstream_error | A service Mosana relies on (payments, images, video, storage) answered with an error. |
| 503 | unavailable, not_configured | Mosana or one of its services can't be reached right now, or isn't switched on yet. Try again shortly. |