# sorai analytics overview (/cli/commands/analytics/overview) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Days are UTC. Revenue is never converted between currencies. ```sh sorai analytics overview [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--from `: Start of the range, inclusive. Defaults to 30 days before to. An RFC 3339 date-time (2026-08-29T00:00:00Z), a bare YYYY-MM-DD date, or a date-time with no seconds. - `--to `: End of the range, inclusive. Defaults to today. An RFC 3339 date-time (2026-08-29T00:00:00Z), a bare YYYY-MM-DD date, or a date-time with no seconds. ## Permissions [#permissions] Needs a key with the `analytics:read` permission. # sorai auth status (/cli/commands/auth/status) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai auth status [flags] ``` ## Flags [#flags] - `--profile `: The saved login to show. ## Permissions [#permissions] Works with any signed-in key. # sorai bundles add (/cli/commands/bundles/add) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} product\_id is the bundle; a bundle goes on sale only with at least one component. ```sh sorai bundles add --product-id --component-product-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--component-product-id ` (required): The product to include, from `sorai products list`. It cannot be a bundle. - `--quantity `: How many of it the buyer gets. Defaults to 1. - `--position ` - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. Run it with `--dry-run` first to see what it would do. # sorai bundles list (/cli/commands/bundles/list) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai bundles list --product-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. ## Permissions [#permissions] Needs a key with the `catalog:read` permission. # sorai bundles remove (/cli/commands/bundles/remove) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} An active bundle keeps at least one component. ```sh sorai bundles remove --product-id --component-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--component-id ` (required): The component's id, from `sorai bundles list`. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai bundles update (/cli/commands/bundles/update) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Only the fields you pass change. ```sh sorai bundles update --product-id --component-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--component-id ` (required): The component's id, from `sorai bundles list`. - `--quantity ` - `--position ` - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:read` and `catalog:write` permissions. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai checkout create (/cli/commands/checkout/create) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} The link's url is returned only once, so keep it. Paying through it runs the normal fulfilment: files, license keys and library access. ```sh sorai checkout create --price-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--price-id ` (required): A price of one of this store's active products, from `sorai prices list`. - `--quantity `: Defaults to 1. - `--buyer-email `: Prefills the buyer's email. The buyer can still change it. - `--metadata `: Up to 20 string values, returned on the order's webhooks. Keys use letters, digits, \_, . and - (40 at most). - `--expires-in-seconds `: How long the link stays open. Defaults to 86400 (one day). - `--allow-discount-codes`: Let the buyer apply a discount code. Off by default. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `checkout:write` permission. This command changes data. Run it with `--dry-run` first to see what it would do. # sorai courses add-lesson (/cli/commands/courses/add-lesson) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} A video or file lesson takes a file uploaded to the product first, or for a video an external\_link. A lesson is hidden from buyers until published is true, and a course goes on sale only with at least one published lesson. ```sh sorai courses add-lesson --product-id --section-id --kind --title [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--section-id ` (required): The section's id, from `sorai courses get`. - `--kind ` (required) - `--title ` (required) - `--source `: file (the default) or external\_link, for a YouTube or Vimeo video. - `--file-id `: A file uploaded to this product, from `sorai products files`. - `--external-url `: With source external\_link: the video's address. - `--body `: A text lesson's content. - `--duration-seconds ` - `--preview`: Let anyone watch this lesson before buying. - `--published`: Show the lesson to buyers. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. Run it with `--dry-run` first to see what it would do. # sorai courses add-section (/cli/commands/courses/add-section) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai courses add-section --product-id --title [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--title ` (required) - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. Run it with `--dry-run` first to see what it would do. # sorai courses delete-lesson (/cli/commands/courses/delete-lesson) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai courses delete-lesson --product-id --lesson-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--lesson-id ` (required): The lesson's id, from `sorai courses get`. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai courses delete-section (/cli/commands/courses/delete-section) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai courses delete-section --product-id --section-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--section-id ` (required): The section's id, from `sorai courses get`. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai courses get (/cli/commands/courses/get) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai courses get --product-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. ## Permissions [#permissions] Needs a key with the `catalog:read` permission. # sorai courses rename-section (/cli/commands/courses/rename-section) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai courses rename-section --product-id --section-id --title [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--section-id ` (required): The section's id, from `sorai courses get`. - `--title ` (required) - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai courses update-lesson (/cli/commands/courses/update-lesson) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Only the fields you pass change; the kind cannot change. Giving a new file\_id, or switching to an external\_link, deletes the lesson's old upload. ```sh sorai courses update-lesson --product-id --lesson-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--lesson-id ` (required): The lesson's id, from `sorai courses get`. - `--title ` - `--source ` - `--file-id ` - `--external-url ` - `--body ` - `--duration-seconds ` - `--preview` - `--published` - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:read` and `catalog:write` permissions. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai guide (/cli/commands/guide) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} It is generated from the binary, so it always matches the version you run. ```sh sorai guide ``` # Command reference (/cli/commands) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Every command below takes `--profile ` to pick a saved login. Output is JSON on stdout. ## General [#general] - [`sorai login`](/cli/commands/login): Sign in with a secret key (ssk\_...) from the API keys page of your Sorai console. - [`sorai logout`](/cli/commands/logout): Forget the saved key on this machine. - [`sorai auth status`](/cli/commands/auth/status): Show the signed-in key: its name, prefix, scopes and stores, the default store, and whether it came from the saved login or `SORAI_API_KEY`. - [`sorai whoami`](/cli/commands/whoami): Show the key's name, scopes and stores, and the API it acts on. - [`sorai guide`](/cli/commands/guide): Print a Markdown guide to every command and flag, for AI agents. - [`sorai version`](/cli/commands/version): Print the version of sorai. ## stores [#stores] - [`sorai stores list`](/cli/commands/stores/list): List the stores this key's organization runs, with each store's id, slug, name, currency and status. - [`sorai stores get`](/cli/commands/stores/get): Get one store's settings: theme, notifications, enabled payment methods and tracking pixels. ## products [#products] - [`sorai products list`](/cli/commands/products/list): List a store's products, newest first, with the total for paging. - [`sorai products get`](/cli/commands/products/get): Get one product: its fields, selling content and fulfilment settings. - [`sorai products create`](/cli/commands/products/create): Create a product. - [`sorai products files`](/cli/commands/products/files): List the files buyers get from a product, with each one's status. - [`sorai products delete-file`](/cli/commands/products/delete-file): Remove a file from a product. - [`sorai products update`](/cli/commands/products/update): Change some of a product's fields. - [`sorai products archive`](/cli/commands/products/archive): Take a product off sale. - [`sorai products upload-file`](/cli/commands/products/upload-file): Upload a file buyers get after paying, such as an ebook or a zip, to a product. ## prices [#prices] - [`sorai prices list`](/cli/commands/prices/list): List a product's prices. - [`sorai prices create`](/cli/commands/prices/create): Add a price to a product. - [`sorai prices update`](/cli/commands/prices/update): Change some of a price's fields. - [`sorai prices delete`](/cli/commands/prices/delete): Remove a price from a product. ## media [#media] - [`sorai media list`](/cli/commands/media/list): List a store's uploaded images, newest first. - [`sorai media upload`](/cli/commands/media/upload): Upload an image (JPEG, PNG, WebP or GIF, up to 20 MB) to the store's media library. ## courses [#courses] - [`sorai courses get`](/cli/commands/courses/get): Get a course product's sections and lessons in order, with each lesson's file status. - [`sorai courses add-section`](/cli/commands/courses/add-section): Add a section to the end of a course. - [`sorai courses rename-section`](/cli/commands/courses/rename-section): Change a section's title. - [`sorai courses delete-section`](/cli/commands/courses/delete-section): Delete a section and every lesson in it. - [`sorai courses add-lesson`](/cli/commands/courses/add-lesson): Add a lesson to the end of a section. - [`sorai courses update-lesson`](/cli/commands/courses/update-lesson): Change some of a lesson's fields, such as published. - [`sorai courses delete-lesson`](/cli/commands/courses/delete-lesson): Delete a lesson. ## bundles [#bundles] - [`sorai bundles list`](/cli/commands/bundles/list): List the products inside a bundle. - [`sorai bundles add`](/cli/commands/bundles/add): Put a product from the same store into a bundle. - [`sorai bundles update`](/cli/commands/bundles/update): Change a component's quantity or position. - [`sorai bundles remove`](/cli/commands/bundles/remove): Take a product out of a bundle. ## orders [#orders] - [`sorai orders list`](/cli/commands/orders/list): List a store's orders, newest first, with the total for paging. - [`sorai orders get`](/cli/commands/orders/get): Get one order, with the buyer, attribution and payment detail. ## checkout [#checkout] - [`sorai checkout create`](/cli/commands/checkout/create): Open a checkout link for one price of an active product, to send to a buyer. ## pages [#pages] - [`sorai pages list`](/cli/commands/pages/list): List a store's pages with their slug, kind and publish state. - [`sorai pages get`](/cli/commands/pages/get): Get one page: its slug, kind, SEO metadata and header and footer setting. - [`sorai pages update`](/cli/commands/pages/update): Change a page's slug, kind, SEO metadata or header and footer setting. - [`sorai pages create`](/cli/commands/pages/create): Create a page as an unpublished draft with no content yet. ## analytics [#analytics] - [`sorai analytics overview`](/cli/commands/analytics/overview): Revenue by currency, orders, visitors, and the view, checkout and purchase funnel for a date range. # sorai login (/cli/commands/login) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} The key is checked, then saved on this machine, readable only by you, so every later command uses it. With more than one store you pick the default `--store`. ```sh sorai login [flags] ``` Also available as `sorai auth login`. ## Flags [#flags] - `--with-key`: Read the key from stdin instead of asking, for scripts and CI. - `--store `: The default store to save. - `--profile `: Save under this name (default "default"). Pick it later with `--profile` or `SORAI_PROFILE`. # sorai logout (/cli/commands/logout) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} The key keeps working until you revoke it in the console. ```sh sorai logout [flags] ``` Also available as `sorai auth logout`. ## Flags [#flags] - `--profile `: The saved login to forget. Defaults to `SORAI_PROFILE`, or the default login. # sorai media list (/cli/commands/media/list) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} A media id is what cover\_media\_id and gallery\_media\_ids take. ```sh sorai media list [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--limit `: How many rows to return, up to 200. - `--cursor `: The pagination.next\_cursor of the previous page. ## Permissions [#permissions] Needs a key with the `catalog:read` permission. # sorai media upload (/cli/commands/media/upload) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Use its id as a product's cover\_media\_id or in gallery\_media\_ids. ```sh sorai media upload --file [flags] ``` ## Flags [#flags] - `--file ` (required): The file on this machine. - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--dry-run`: Print what would be uploaded without uploading it. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. Run it with `--dry-run` first to see what it would do. # sorai orders get (/cli/commands/orders/get) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai orders get --order-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--order-id ` (required): The order's id, from `sorai orders list`. ## Permissions [#permissions] Needs a key with the `orders:read` permission. # sorai orders list (/cli/commands/orders/list) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Every filter is optional. ```sh sorai orders list [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--status ` - `--currency `: An ISO 4217 code, such as USD or IDR. - `--from `: Orders created at or after this time. An RFC 3339 date-time (2026-08-29T00:00:00Z), a bare YYYY-MM-DD date, or a date-time with no seconds. - `--until `: Orders created before this time. An RFC 3339 date-time (2026-08-29T00:00:00Z), a bare YYYY-MM-DD date, or a date-time with no seconds. - `--q `: Matches part of an order number or buyer email. - `--limit `: How many rows to return, up to 200. - `--offset `: How many rows to skip, for the next page. ## Permissions [#permissions] Needs a key with the `orders:read` permission. # sorai pages create (/cli/commands/pages/create) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Its content is built and published from the console. ```sh sorai pages create --slug --kind [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--slug ` (required) - `--kind ` (required) - `--mode `: blocks uses the page builder; html is hand-written markup. - `--seo `: SEO metadata: title, description, og\_image\_media\_id, canonical, noindex. Replaces the whole object, so read it first. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `pages:write` permission. This command changes data. Run it with `--dry-run` first to see what it would do. # sorai pages get (/cli/commands/pages/get) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai pages get --page-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--page-id ` (required): The page's id, from `sorai pages list`. ## Permissions [#permissions] Needs a key with the `pages:read` permission. # sorai pages list (/cli/commands/pages/list) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai pages list [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. ## Permissions [#permissions] Needs a key with the `pages:read` permission. # sorai pages update (/cli/commands/pages/update) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Only the fields you pass change. Does not touch the page's content: that is drafted and published from the console. ```sh sorai pages update --page-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--page-id ` (required): The page's id, from `sorai pages list`. - `--slug ` - `--kind ` - `--seo `: SEO metadata: title, description, og\_image\_media\_id, canonical, noindex. Replaces the whole object, so read it first. - `--chrome `: true or false forces the store header and footer on or off for this page; null goes back to the default. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `pages:read` and `pages:write` permissions. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai prices create (/cli/commands/prices/create) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} A product has one price per currency, in a currency the store can take payments in. ```sh sorai prices create --product-id --currency --amount-minor --kind [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--currency ` (required): An ISO 4217 code, such as IDR or USD. - `--amount-minor ` (required): The amount in the currency's smallest unit: 1500 is USD 15.00. IDR, JPY, KRW and VND have no decimals, so 150000 is IDR 150,000. - `--kind ` (required): one\_time, or recurring for a membership. - `--interval `: For a recurring price: month or year. - `--pay-what-you-want`: Let the buyer choose the amount, starting from min\_minor. - `--min-minor `: With pay\_what\_you\_want: the lowest amount, in the same units as amount\_minor. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. Run it with `--dry-run` first to see what it would do. # sorai prices delete (/cli/commands/prices/delete) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} An active product keeps at least one price. ```sh sorai prices delete --product-id --price-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--price-id ` (required): The price's id, from `sorai prices list`. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai prices list (/cli/commands/prices/list) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Amounts are in minor units of their currency (cents, not dollars). A price id is what `sorai checkout create` takes. ```sh sorai prices list --product-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. ## Permissions [#permissions] Needs a key with the `catalog:read` permission. # sorai prices update (/cli/commands/prices/update) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Only the fields you pass change; the currency cannot change. ```sh sorai prices update --product-id --price-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--price-id ` (required): The price's id, from `sorai prices list`. - `--amount-minor `: The amount in the currency's smallest unit: 1500 is USD 15.00. IDR, JPY, KRW and VND have no decimals, so 150000 is IDR 150,000. - `--kind ` - `--interval `: For a recurring price: month or year. - `--pay-what-you-want ` - `--min-minor ` - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:read` and `catalog:write` permissions. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai products archive (/cli/commands/products/archive) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Buyers who already bought it keep their access; `sorai products update` with status active puts it back on sale. ```sh sorai products archive --product-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai products create (/cli/commands/products/create) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} It starts as a draft; add a price with `sorai prices create`, then set its status to active with `sorai products update` to put it on sale. ```sh sorai products create --slug --name --kind [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--slug ` (required): The product's address in the store, such as my-ebook. - `--name ` (required) - `--kind ` (required) - `--description `: The short description. - `--cover-media-id `: An image from `sorai media list` to use as the cover. - `--thank-you-url `: An absolute https url the buyer goes to after paying. - `--settings `: The product page's selling content (description markdown, gallery\_media\_ids, bullets, testimonials, faq). - `--license-config `: For a license product: activation\_limit and instructions. - `--service-config `: For a service product: instructions and booking\_url. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. Run it with `--dry-run` first to see what it would do. # sorai products delete-file (/cli/commands/products/delete-file) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Buyers lose access to it. ```sh sorai products delete-file --product-id --file-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--file-id ` (required): The file's id, from `sorai products files`. - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai products files (/cli/commands/products/files) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} A file turns from uploading to ready once it is checked, or to rejected when the check fails. ```sh sorai products files --product-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. ## Permissions [#permissions] Needs a key with the `catalog:read` permission. # sorai products get (/cli/commands/products/get) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai products get --product-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. ## Permissions [#permissions] Needs a key with the `catalog:read` permission. # sorai products list (/cli/commands/products/list) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Pass q to find a product by part of its name or slug. ```sh sorai products list [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--q `: Matches part of a product's name or slug, ignoring case. - `--status ` - `--limit `: How many rows to return, up to 200. - `--offset `: How many rows to skip, for the next page. ## Permissions [#permissions] Needs a key with the `catalog:read` permission. # sorai products update (/cli/commands/products/update) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Only the fields you pass change; everything else keeps its current value. ```sh sorai products update --product-id [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--slug ` - `--name ` - `--kind ` - `--status `: active puts the product on sale; archived takes it off. - `--description `: The short description. An empty string clears it. - `--cover-media-id `: The cover image's media id, or null to remove the cover. - `--thank-you-url `: An absolute https url the buyer goes to after paying. An empty string clears it. - `--settings `: The product page's selling content (description markdown, gallery\_media\_ids, bullets, testimonials, faq). Replaces the whole object, so read it first. - `--license-config ` - `--service-config ` - `--dry-run`: Print the request it would send, and for an update the fields that would change, without changing anything. ## Permissions [#permissions] Needs a key with the `catalog:read` and `catalog:write` permissions. This command changes data. It can overwrite, archive or delete what is stored. Run it with `--dry-run` first to see what it would do. # sorai products upload-file (/cli/commands/products/upload-file) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Large files go up in parts. The file is checked after it uploads, and its status turns from uploading to ready. ```sh sorai products upload-file --file --product-id [flags] ``` ## Flags [#flags] - `--file ` (required): The file on this machine. - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. - `--product-id ` (required): The product's id, from `sorai products list`. - `--version `: Which version of the file this is. Defaults to 1. - `--dry-run`: Print what would be uploaded without uploading it. ## Permissions [#permissions] Needs a key with the `catalog:write` permission. This command changes data. Run it with `--dry-run` first to see what it would do. # sorai stores get (/cli/commands/stores/get) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Secret tokens come back masked. ```sh sorai stores get [flags] ``` ## Flags [#flags] - `--store `: A store id or slug. Defaults to `SORAI_STORE`, the saved login's store, or the only store. ## Permissions [#permissions] Needs a key with the `pages:read` permission. # sorai stores list (/cli/commands/stores/list) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} Every other tool takes a `--store` from here. ```sh sorai stores list ``` ## Permissions [#permissions] Works with any signed-in key. # sorai version (/cli/commands/version) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai version ``` Also available as `sorai --version`. # sorai whoami (/cli/commands/whoami) {/* Generated from the sorai command definitions by `make docs-cli`. Edit the CLI, not this page. */} ```sh sorai whoami ``` ## Permissions [#permissions] Works with any signed-in key. # Overview (/cli) `sorai` lists and updates products, prices, pages and orders, opens checkout links and reads sales analytics, from your terminal. Every command prints JSON, so it works as well in a script or for an AI agent as it does for you. It is one small binary with no runtime to install. New here? The [quickstart](/cli/quickstart) takes you from install to a product on sale. Every command and flag is in the [command reference](/cli/commands). ## Install [#install] ```sh brew install sorai-ai/tap/sorai ``` Or, on macOS and Linux, into `~/.local/bin`: ```sh curl -fsSL https://sorai.ai/install.sh | sh ``` On Windows, download the zip for your CPU from [the releases page](https://github.com/sorai-ai/cli/releases). Check it runs with `sorai version`. ## Sign in [#sign-in] The CLI acts with a secret key. Create one in the Sorai console under **Organization**, **API keys**: pick **Secret** and the permissions the key needs. Its permissions decide what `sorai` can do. Then: ```sh sorai login ``` `sorai login` asks for the key without showing it, checks it, lets you pick a default store when you have several, and saves it on this machine, readable only by you. Every later command uses it. - `sorai auth status` shows the key you are signed in with. - `sorai logout` forgets the key on this machine. The key keeps working until you revoke it in the console. - For more than one account, sign in with `sorai login --profile work`, then pass `--profile work` (or set `SORAI_PROFILE=work`) on any command. The key is never printed. ## Scripts and CI [#scripts-and-ci] Pipe the key in with `--with-key`, or set `SORAI_API_KEY`. When `SORAI_API_KEY` is set, the saved login is not used at all. ```sh echo "$SORAI_KEY" | sorai login --with-key ``` | Variable | What it does | | --------------- | -------------------------------------------------------------- | | `SORAI_API_KEY` | The secret key every request uses, instead of the saved login. | | `SORAI_STORE` | The default `--store`, as an id or a slug. | | `SORAI_PROFILE` | The saved login to use. | ## How commands behave [#how-commands-behave] - Commands read `sorai --flags`. Every input is a flag. - `--store` takes a store id or a slug. Left out, it uses `SORAI_STORE`, the store saved at login, or your only store. - Output is JSON on stdout: indented on a terminal, one line in a pipe. Filter it with `jq`. - A failure writes `{"error":{"code","message"}}` to stderr. Exit code 2 means the command or a flag was wrong; exit code 1 means Sorai refused the request. - Flag values are whole numbers, `true` or `false`, `null` to clear a field that allows it, and JSON for object flags such as `--settings`. Pass an empty value as `--description=""`. - Updates change only the flags you pass. A JSON flag replaces the whole object, so read it first, change it, and send it back. - Every command that changes data takes `--dry-run`, which prints what it would do and changes nothing. - Amounts are in the currency's smallest unit: 1500 is USD 15.00, and since IDR has no decimals, 150000 is IDR 150,000. Sorai never converts between currencies. ## For AI agents [#for-ai-agents] `sorai guide` prints a Markdown guide to every command and flag, generated from the binary you run. Point your agent at it. Each release also ships a `SKILL.md` next to the binary for agent harnesses that read skills. ## Verify a release [#verify-a-release] Each release's `checksums.txt` is signed with Sorai's release key. Fetch the public key from sorai.ai, not from where you downloaded the release, then check with [cosign](https://docs.sigstore.dev): ```sh curl -fsSLO https://sorai.ai/cosign.pub cosign verify-blob checksums.txt --bundle checksums.txt.sigstore.json --key cosign.pub shasum -a 256 -c --ignore-missing checksums.txt ``` `install.sh` checks each download against `checksums.txt`; the signature is what proves a release is genuine. # Quickstart (/cli/quickstart) This walks through selling an ebook from your terminal: install `sorai`, sign in, add a product with a price and a file, give it a page, and publish. It takes about ten minutes. ## 1. Install [#1-install] ```sh brew install sorai-ai/tap/sorai ``` No Homebrew? Use `curl -fsSL https://sorai.ai/install.sh | sh` on macOS and Linux, or see [Install](/cli#install) for Windows. Then check it runs: ```sh sorai version ``` ## 2. Create a store [#2-create-a-store] Stores are created in the console. Sign up at [console.sorai.ai](https://console.sorai.ai): the first-run setup asks for your business and creates your first store. Already have one? Skip ahead. To add another store later, use **New store** in the store switcher. ## 3. Create a secret key [#3-create-a-secret-key] In the console, open **Organization**, then **API keys**, and choose **Create key**. Pick **Secret**, and give it these permissions for this guide: - View products, prices and discount codes - Manage products, prices, media and discount codes - View stores and pages - Manage stores, themes and pages, including publishing Copy the key (it starts with `ssk_`). It is shown only once. ## 4. Sign in [#4-sign-in] ```sh sorai login ``` Paste the key when asked; it is not shown as you type. With more than one store, pick the one commands should use by default. Check who you are signed in as: ```sh sorai auth status ``` The commands below leave out `--store` and use that default. Add `--store ` to any of them to work on another store. ## 5. Create a product [#5-create-a-product] Upload a cover image. The command prints the new image's `id`: ```sh sorai media upload --file cover.png ``` Create the product with that image as its cover. It starts as a draft, so nobody can buy it yet. Note the product `id` it prints: ```sh sorai products create --slug my-ebook --name "My Ebook" --kind file \ --cover-media-id ``` Give it a price. Amounts are in the currency's smallest unit, so for IDR 150,000: ```sh sorai prices create --product-id --currency IDR \ --amount-minor 150000 --kind one_time ``` Upload the file buyers get after paying, then check on it until its status is `ready`: ```sh sorai products upload-file --product-id --file my-ebook.pdf sorai products files --product-id ``` ## 6. Create a page [#6-create-a-page] Create a landing page for the product. It starts as an unpublished draft with no content: ```sh sorai pages create --slug my-ebook --kind landing --mode blocks ``` Its content is built in the console: open **Pages**, pick `my-ebook`, and add your blocks in the page builder. ## 7. Publish [#7-publish] Put the product on sale. Try it with `--dry-run` first, which shows the change and makes none: ```sh sorai products update --product-id --status active --dry-run sorai products update --product-id --status active ``` Then publish the page from the page builder in the console with **Publish**. Your product is live, and `sorai products list` shows it as `active`. ## Next steps [#next-steps] - Send a buyer straight to checkout with [`sorai checkout create`](/cli/commands/checkout/create). The key needs the "Create checkout links" permission. - Follow sales with [`sorai orders list`](/cli/commands/orders/list) and [`sorai analytics overview`](/cli/commands/analytics/overview). - Browse every command in the [command reference](/cli/commands). # Introduction (/) Sorai is where creators sell digital products. These docs are for developers who want to work with a Sorai store from code: from a terminal, from a site they host themselves, from their own backend, or from an AI assistant. Running a store from the console instead? The [Help Center](/help) covers that. ## What is here [#what-is-here] The docs grow one section at a time. Each section in the sidebar marked **Soon** is on its way. - **CLI**: manage a store from your terminal. - **sorai-js**: open checkout and track events on a site you host yourself. - **API reference**: every public endpoint, and the webhooks Sorai sends. - **MCP**: connect an AI assistant to your store. ## For AI tools [#for-ai-tools] Every page is also published as plain text. [`/llms.txt`](/llms.txt) lists the pages, and [`/llms-full.txt`](/llms-full.txt) holds all of them in one file. # Coming soon (/mcp) Sorai is building an MCP server. [MCP](https://modelcontextprotocol.io) (the Model Context Protocol) is an open standard that lets an AI assistant use tools from outside the chat. Once the Sorai server is available, you can connect it to any assistant that supports MCP and work with your store in plain language. ## What it will let you do [#what-it-will-let-you-do] - **Ask about your store.** Look up products, prices and orders, and check how sales are going, without opening the dashboard. - **Set up products.** Create a product, write its description and set its price from a conversation. - **Build pages.** Draft a page, then refine it a request at a time. - **Share checkout links.** Get a link that takes a buyer straight to checkout. The assistant acts only with the access you give it, and you can take that access away at any time. ## Get notified [#get-notified] This page becomes the MCP guide when the server is ready: how to connect your assistant, and every tool it can use. To follow along: - Come back to this page. It changes as soon as the MCP is available. - Watch [`/llms.txt`](/llms.txt). It lists every page on this site, so new MCP pages show up there the moment they are published. # Opening checkout (/sorai-js/checkout) ```js Sorai.checkout({ productId: "0199b0f0-0000-7000-8000-000000000021", priceId: "0199b0f0-0000-7000-8000-000000000031", quantity: 2, }); ``` `priceId` picks the currency: a product has one price per currency. Leave it off and the store's default currency is used. `quantity` defaults to 1. The first time a tab opens checkout for a product, the runtime also sends an `InitiateCheckout` event for it. ## Modal or full page [#modal-or-full-page] On a viewport 768px or wider, checkout opens in a modal over your page: a backdrop, a panel, and the checkout inside it with its own close button. Only that button closes it. Escape, a click on the backdrop and a finished payment all leave it open, so a buyer never loses a payment code or a receipt they are still reading. Your page stops scrolling while the modal is up, and focus returns to the control that opened it. Below 768px the whole page goes to checkout instead. A phone has no room for a modal, and a framed checkout on a phone breaks the wallet apps a payment may hand off to. ## Knowing when the buyer paid [#knowing-when-the-buyer-paid] When a payment completes inside the modal, the runtime dispatches a `sorai:paid` event on `window`. The modal stays open on the receipt. ```js window.addEventListener("sorai:paid", () => { document.body.classList.add("thank-you"); }); ``` The event is a signal for your page, such as showing a thank-you state. It is sent from the buyer's browser, so it is not proof of payment: confirm a sale from your server with webhooks before you deliver anything. ## Hidden fields [#hidden-fields] `fields` fills the store's hidden checkout fields, for tags of your own: ```js Sorai.checkout({ productId: "0199b0f0-0000-7000-8000-000000000021", fields: { campaign: "spring" }, }); ``` The values are saved on the order as `custom_fields` and come back in every order webhook. Keys that are not one of the store's hidden fields are dropped. Query parameters on the page's own URL are passed the same way, and `fields` wins when both name the same key. The values come from the buyer's browser, so the buyer can change them. Never use them to decide whose account to credit: for that, create a checkout session from your server and send the buyer to its link. UTM parameters need no hidden field. The runtime records them as attribution, and they arrive on the order with it. # Custom elements (/sorai-js/elements) sorai-js defines three custom elements. They work in any HTML once `Sorai.init()` has run. ```html Get the preset

Preset Senja

``` ## `` [#sorai-buy] A buy button. A click calls `Sorai.checkout()` for its product. | Attribute | Description | | ------------ | -------------------------------------------------------------------------------- | | `product-id` | The product to buy. Required. | | `price-id` | The price to charge. Optional; without it, the store's default currency is used. | With no content of its own, the element reads "Buy now". ## `` [#sorai-price] Shows one of the product's prices, formatted in the store's `locale`. | Attribute | Description | | ------------ | ------------------------------- | | `product-id` | The product to price. Required. | | `currency` | The currency to show. Optional. | The price shown is the one in the element's `currency`, else in the `currency` given to `init()`, else the product's first price. Sorai never converts between currencies: a product with no price in the asked-for currency shows a price it does have. The element replaces its content only once the product has loaded. Put a fallback inside it, such as the price your page was built with, and it stays there if the request fails or the product is gone. ## `` [#sorai-link] Links its content to the product's own page on the store, `/p/{slug}`. | Attribute | Description | | ------------ | --------------------------------- | | `product-id` | The product to link to. Required. | It is keyed on the product id, not the slug, so renaming a product's slug never breaks a link: the runtime loads the product and builds the link from whatever slug it has now. Once the product loads, the element moves its content inside an ``, so style it with the selector `sorai-link a`. An unknown or inactive product, or a failed request, leaves the content as plain markup and never links to a missing page. The link is relative, so it belongs on a page of the store itself. ## Putting them together [#putting-them-together] Use `` for "see the product" and `` for "buy now". A product card commonly carries both: one `` around the cover and the name, and a buy button beside it. Never put the button inside the link, or a click on it would also follow the link. ```html

Preset Senja

Rp 149.000
``` `` and `` share one request per product per page, so a grid with a price and a link on every card loads each product once. ## Events the elements send [#events-the-elements-send] `` and `` send `ViewContent` for their product when they appear on the page, once per product per page load: a price and a buy button for the same product send one event between them. `` sends nothing, because the product page it opens sends its own. # Events and attribution (/sorai-js/events) Every event goes to Sorai, which counts it in the store's analytics, and to each pixel you gave `init()`: Meta, TikTok, Google Analytics 4 and a `dataLayer` push for Google Tag Manager. ## Standard events [#standard-events] | Event | Sent when | | ------------------ | ----------------------------------------------------------------------------------------- | | `PageView` | `init()` runs on a page with a page id. | | `ViewContent` | A `` or `` for a product appears, once per product per page load. | | `InitiateCheckout` | Checkout opens for a product, once per product per tab session. | `InitiateCheckout` stays in Sorai's own analytics and is not forwarded to your pixels; the ad platforms hear about checkouts and purchases from Sorai's server instead. On a page that shows a product without the custom elements, send its `ViewContent` yourself. It gets the same id and the same once per page deduplication as the elements: ```js Sorai.track("ViewContent", { product_id: "0199b0f0-0000-7000-8000-000000000021" }); ``` ## Custom events [#custom-events] Any other name is a custom event, with whatever properties you pass: ```js Sorai.track("AddedToWishlist", { product_id: "0199b0f0-0000-7000-8000-000000000021" }); ``` A Meta standard event name is sent to Meta as a standard event; anything else as a custom one. ## Counting each event once [#counting-each-event-once] When a sale or a view is reported to Meta or TikTok from both the browser and Sorai's server, the platform has to recognise the two as one. So `PageView` and `ViewContent` get **deterministic** ids built from the tab session and the page or product: - `PageView`: `{session}-{page}-pv` - `ViewContent`: `{session}-{product}-vc` The server sends the same ids, and the platform counts the event once. Custom events get a random id, because the server sends no copy of them. ## Sessions and attribution [#sessions-and-attribution] - **Session.** A random id in `sessionStorage` (`_sorai_sid`), one per tab. - **Visitor.** A random id in a first-party cookie (`_sorai_vid`) that lasts a year, so a new tab or a return visit is the same visitor. - **Attribution.** Recorded once, at the first `init()` that finds no attribution stored: the `utm_*` parameters, `fbclid`, `ttclid`, the referrer and the landing page. It is kept in a first-party cookie (`_sorai_attr`) for 90 days and never overwritten by a later visit: the first touch wins. - **Referral code.** A visit that arrives with `?ref=CODE` stores the code for 30 days (`_sorai_ref`). Here the last click wins: a new code replaces the old one and restarts the 30 days. The Meta, TikTok and Google Analytics browser ids (`_fbp`, `_ttp` and `_ga`) are read from their cookies each time an event is sent, because those scripts may not have run yet when `init()` did. `fbclid` is also stored in the `fb.1.{timestamp}.{fbclid}` form Meta expects. Attribution and the referral code travel with the buyer into checkout, so the order records both. # Overview (/sorai-js) sorai-js is the small browser runtime, under 4 KB gzipped, that every Sorai store page loads. It does three things: 1. **Opens checkout** from any element on the page, carrying the buyer's attribution with it. 2. **Records attribution** on the buyer's first landing and keeps it for 90 days, so a purchase two visits later still credits the campaign that earned it. 3. **Tracks events** to Sorai and to the ad and analytics tags you configure: Meta, TikTok, Google Analytics 4 and Google Tag Manager. It has no framework and no dependencies, and there is nothing to build. ## On a Sorai store page [#on-a-sorai-store-page] Every page of a Sorai store already loads sorai-js and calls `Sorai.init()` with the store's own settings, before the page body. That includes a page you write yourself in HTML. You can use the [custom elements](/sorai-js/elements) in your markup and call `window.Sorai` from your scripts straight away: ```html Buy now ``` ## Configuring the runtime yourself [#configuring-the-runtime-yourself] Where nothing calls `init()` for you, call it once, before anything else and before the body, so the custom elements find it configured when they appear: ```html ``` `init()` is synchronous and cheap. It stores the configuration, records attribution, loads whichever pixel scripts you configured and sends a `PageView` for the page. Every option is listed in the [reference](/sorai-js/reference#soraiinitconfig). - `currency` is the store's default currency (ISO 4217). It picks which price `` shows. Without it, the element shows the product's first price. - `locale` is the BCP 47 tag prices are formatted in. Without it, the browser's own locale decides. ### Loading the runtime [#loading-the-runtime] Load it from `https://js.sorai.ai/v1/sorai.js`, as a plain blocking script in ``. Everything under `/v1/` stays compatible: new options and fixes ship to the same address, and anything that would break a page gets a new version path instead. Browsers cache it for a few minutes, so a fix reaches your site without a change on your side. Link to it rather than copying the file, so you get those fixes. Leave out an `integrity` hash for the same reason: the file changes with each compatible release, and a pinned hash would stop it loading. ### The publishable key [#the-publishable-key] `publicKey` is a **publishable** key (`spk_...`), scoped to one store. It can read that store's public catalogue and start a checkout, nothing else, so it is safe in page source. Create one under **Organization**, **API keys** in the Sorai dashboard. A secret key (`ssk_...`) must never appear in a page. Anyone who views the source can read it. ### Naming the page [#naming-the-page] Set `window.SORAI_PAGE_ID` before the runtime loads to attribute events to a particular page. Sorai store pages set it for you. ```html ``` The value is a page id, or `product:` followed by a product id for a product's own page. Without it, no `PageView` is sent and other events carry no page; nothing else changes. Any other value is rejected: the event is answered with `422 invalid_page_id` instead of being recorded. ## Next [#next] - [Opening checkout](/sorai-js/checkout): the modal, hidden fields and the paid event. - [Custom elements](/sorai-js/elements): buy buttons, prices and product links with no JavaScript of your own. - [Events and attribution](/sorai-js/events): what is sent, and how it avoids double counting. - [Reading products](/sorai-js/products): product data and prices in minor units. - [Reference](/sorai-js/reference): every method and type. # Reading products (/sorai-js/products) ```js const product = await Sorai.product("0199b0f0-0000-7000-8000-000000000021"); if (product) { console.log(product.name, product.prices); } ``` `Sorai.product()` loads one product of the store with the publishable key. It resolves to the product, or to `null` when the store has no active product with that id, and it rejects when the request fails. The fields are listed in the [reference](/sorai-js/reference#soraiproduct); any other field the API returns comes through as it is. ## Prices [#prices] A product has one price per currency, and Sorai never converts between them. Pick the price for the currency you want to show, and fall back to one the product does have: ```js const product = await Sorai.product("0199b0f0-0000-7000-8000-000000000021"); const price = product?.prices.find((p) => p.currency === "IDR") ?? product?.prices[0]; ``` ## Minor units [#minor-units] Amounts are integers in the currency's **minor units**, and how many minor units make one major unit depends on the currency. `amount_minor: 149000` is Rp 149.000 in IDR, which has no subdivision, but $1,490.00 in USD. Dividing by 100 is wrong for every currency without two decimals. To show a price, divide by the currency's own factor: ```js const minorDigits = { IDR: 0, JPY: 0, KRW: 0, VND: 0, BHD: 3, KWD: 3, OMR: 3 }; function formatPrice({ amount_minor, currency }, locale) { const digits = minorDigits[currency] ?? 2; return new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: digits, maximumFractionDigits: digits, }).format(amount_minor / 10 ** digits); } formatPrice({ amount_minor: 149000, currency: "IDR" }, "id-ID"); // "Rp 149.000" ``` `` does this for you, for every currency. # Reference (/sorai-js/reference) {/* Generated from the package by `pnpm --filter @sorai/docs gen:sorai-js`. Do not edit by hand. */} The runtime's whole public surface: the `window.Sorai` object and the types its methods take and return. The custom elements are described in [Custom elements](/sorai-js/elements). ## SoraiApi [#soraiapi] `window.Sorai`: the runtime's whole JavaScript API. ### Sorai.init() [#soraiinit] ```ts Sorai.init(config: SoraiInitConfig): void ``` Configures the runtime. Call it once, before anything else. It records first-touch attribution, loads the configured pixels and sends a `PageView` for the page named by `window.SORAI_PAGE_ID`. ### Sorai.checkout() [#soraicheckout] ```ts Sorai.checkout(options: CheckoutOptions): void ``` Opens checkout for a product: in a modal over the page on a viewport 768px or wider, or by navigating to checkout on a narrower one. Sends `InitiateCheckout` once per product per tab session. ### Sorai.track() [#soraitrack] ```ts Sorai.track(name: string, props?: Record): void ``` Sends an event to Sorai and to every configured pixel. `ViewContent` and `InitiateCheckout` with a `product_id` prop are deduplicated per product; any other name is a custom event. ### Sorai.product() [#soraiproduct] ```ts Sorai.product(id: string): Promise ``` Fetches one product with the publishable key. Resolves to `null` when the store has no active product with that id, and rejects when the request fails. ## SoraiInitConfig [#soraiinitconfig] What `Sorai.init()` takes. | Property | Type | Description | | --- | --- | --- | | `storeId` | `string` | The store's id. | | `storeSlug?` | `string` | The store's slug. When set, checkout links name the store by slug instead of by id. | | `publicKey` | `string` | The store's publishable key (`spk_...`). Safe in page source; never put a secret key here. | | `apiBase` | `string` | Base URL of the Sorai API, with no trailing slash. | | `checkoutBase` | `string` | Base URL of the Sorai checkout. | | `currency?` | `string` | The store's default currency (ISO 4217). `` renders the price row matching it unless the element overrides with a `currency` attribute. Omitted, the element falls back to the product's first price row. | | `locale?` | `string` | The store's locale, used to format prices. Omitted, the browser's wins. | | `pixels?` | `PixelsConfig` | The ad and analytics tags to load. | ## PixelsConfig [#pixelsconfig] Ad and analytics tags the runtime loads and forwards every event to. | Property | Type | Description | | --- | --- | --- | | `metaPixelId?` | `string` | Meta Pixel id. | | `tiktokPixelId?` | `string` | TikTok Pixel id. | | `ga4Id?` | `string` | Google Analytics 4 measurement id, such as `G-ABC123`. | | `gtmId?` | `string` | Google Tag Manager container id, such as `GTM-XXXX`. | ## CheckoutOptions [#checkoutoptions] What `Sorai.checkout()` takes. | Property | Type | Description | | --- | --- | --- | | `productId` | `string` | The product to buy. | | `priceId?` | `string` | The price to charge, which picks the currency. Omitted, the store's default currency is used. | | `quantity?` | `number` | How many to buy. Default: `1`. | | `fields?` | `Record` | Values for the store's hidden checkout fields, keyed by field key, such as `{ campaign: "spring" }`. They come back in the order's `custom_fields`; keys the form does not have are dropped. The buyer can change them, so never use them to identify a user: open a checkout session from your server for that. | ## SoraiProduct [#soraiproduct-1] A product as `Sorai.product()` returns it. The fields below are always there; any other field the API returns comes through as it is. | Property | Type | Description | | --- | --- | --- | | `id` | `string` | The product id. | | `slug` | `string` | The product's slug, the last segment of its page URL. | | `name` | `string` | The product's name. | | `description` | `string` | The product's description. | | `prices` | `SoraiPrice[]` | One price per currency the product sells in. | ## SoraiPrice [#soraiprice] One price of a product. A product has one price per currency, and Sorai never converts between them. | Property | Type | Description | | --- | --- | --- | | `id` | `string` | The price id, to pass as `priceId` to `Sorai.checkout()`. | | `currency` | `string` | ISO 4217 currency code. | | `amount_minor` | `number` | The amount in the currency's minor units: `149000` in IDR is Rp 149.000, `1900` in USD is $19.00. | | `kind?` | `string` | `one_time` or `recurring`. | | `pay_what_you_want?` | `boolean` | True when the buyer chooses what to pay. | | `min_minor?` | `number \| null` | The lowest amount a buyer may pay, in minor units, when `pay_what_you_want` is true. | # Overview (/api) The public API is what a storefront, a checkout and a buyer's library call. It reads a store's public data, starts a checkout, follows an order to fulfilment and records tracking events. It is safe to call from a browser. ## Base URL [#base-url] ``` https://api.sorai.ai ``` Every route is under `/v1/public`. Requests and responses are JSON. ## Authentication [#authentication] Send your store's publishable key in the `X-Sorai-Key` header: ```bash curl https://api.sorai.ai/v1/public/products/my-ebook \ -H "X-Sorai-Key: spk_..." ``` A publishable key starts with `spk_` and belongs to one store. It is safe to ship to a browser: it can read public store data, start a checkout and post tracking events, and it can never change the catalog, list orders or read a buyer's personal details. Create one in the Sorai console under **Organization**, **API keys**. Pick **Publishable** and the store it is for. A few routes take something else instead of the key, and each says so on its page: - An order's status and its post-payment offer take the `token` the checkout returned. - The buyer's library uses a session cookie, set when the buyer follows the link Sorai emails them. - Media links accept the key as `?key=`, because an `` cannot set a header. Tracking events accept it in the body as `public_key`, because `navigator.sendBeacon` cannot either. ## Errors [#errors] A request that fails answers a non-2xx status and a body like this: ```json { "error": { "code": "payment_method_required", "message": "choose how you want to pay" } } ``` Branch on `code`; `message` is for people and can change. A request whose body or query fails validation answers `422` with the code `validation_failed` and a `fields` array naming every field that failed. ## Money [#money] Every amount is an integer in the currency's minor unit, such as cents, in a field ending `_minor`. Never a float. Sorai never converts between currencies: a price is charged in the currency it is listed in. ## Retrying a checkout [#retrying-a-checkout] `POST /v1/public/checkout` needs an `Idempotency-Key` header, unique per checkout attempt. Sending the same key again returns the original order instead of creating a second one, so a checkout that timed out can be retried safely. ## Try it [#try-it] Each endpoint page has a playground. Paste your own publishable key into it and send the request: it goes from your browser straight to the API, against your real store. These docs embed no key, and the one you paste stays in this browser tab's session storage until you close it. A checkout sent from the playground creates a real order, so try it on a store you test with. Never paste a secret key (`ssk_`): the playground only needs a publishable one, and a secret key must not reach a browser. ## Webhooks [#webhooks] To hear about orders as they happen instead of asking for them, see [Webhooks](/api/webhooks). # Get a short-lived media URL for a free-preview lesson (/api/reference/catalog/get-course-lesson-media) {/* Generated from the public API spec by make gen. Do not edit. */} # Redirect to a media asset at a given width (/api/reference/catalog/get-media-width) {/* Generated from the public API spec by make gen. Do not edit. */} # Redirect to a media asset (/api/reference/catalog/get-media) {/* Generated from the public API spec by make gen. Do not edit. */} # Get one active product by its store-scoped slug (/api/reference/catalog/get-product) {/* Generated from the public API spec by make gen. Do not edit. */} # Batch-fetch active products by id (/api/reference/catalog/list-products) {/* Generated from the public API spec by make gen. Do not edit. */} # Accept a post-payment offer, creating a second order (/api/reference/checkout/accept-checkout-offer) {/* Generated from the public API spec by make gen. Do not edit. */} # Ask for the order a checkout retry is walking away from to be cancelled (/api/reference/checkout/cancel-order) {/* Generated from the public API spec by make gen. Do not edit. */} # Create an order and its payment session (/api/reference/checkout/create-checkout) {/* Generated from the public API spec by make gen. Do not edit. */} # Get the store's checkout form (/api/reference/checkout/get-checkout-form) {/* Generated from the public API spec by make gen. Do not edit. */} # What a checkout link opened by the merchant's server sells (/api/reference/checkout/get-checkout-session) {/* Generated from the public API spec by make gen. Do not edit. */} # Poll one order, with the token returned from checkout (/api/reference/checkout/get-order-status) {/* Generated from the public API spec by make gen. Do not edit. */} # Read what a link back to an unpaid order fills in (/api/reference/checkout/recover-checkout) {/* Generated from the public API spec by make gen. Do not edit. */} # Stop reminder emails about unpaid orders from one store (/api/reference/checkout/unsubscribe-recovery) {/* Generated from the public API spec by make gen. Do not edit. */} # Exchange a magic-link token for a library session cookie (/api/reference/library/create-library-session) {/* Generated from the public API spec by make gen. Do not edit. */} # Sign out of the library everywhere (/api/reference/library/delete-library-session) {/* Generated from the public API spec by make gen. Do not edit. */} # Redeem a download token for the file (/api/reference/library/download) {/* Generated from the public API spec by make gen. Do not edit. */} # A purchased course, with the buyer's own progress (/api/reference/library/get-library-course) {/* Generated from the public API spec by make gen. Do not edit. */} # Get a short-lived media URL for a purchased lesson (/api/reference/library/get-library-lesson-media) {/* Generated from the public API spec by make gen. Do not edit. */} # Everything the signed-in buyer owns (/api/reference/library/get-library) {/* Generated from the public API spec by make gen. Do not edit. */} # Email a buyer a link back into their library (/api/reference/library/request-library-magic-link) {/* Generated from the public API spec by make gen. Do not edit. */} # Record the buyer's position on a lesson (/api/reference/library/write-lesson-progress) {/* Generated from the public API spec by make gen. Do not edit. */} # Claim one activation slot for an instance (/api/reference/licenses/activate-license) {/* Generated from the public API spec by make gen. Do not edit. */} # Check whether a license key is good (/api/reference/licenses/verify-license) {/* Generated from the public API spec by make gen. Do not edit. */} # The payment rails this store can offer for a currency (/api/reference/payments/list-payment-methods) {/* Generated from the public API spec by make gen. Do not edit. */} # The store's affiliate offer, for its own join page (/api/reference/storefront/get-affiliate-program) {/* Generated from the public API spec by make gen. Do not edit. */} # Get a page's published display (/api/reference/storefront/get-page-display) {/* Generated from the public API spec by make gen. Do not edit. */} # Get the browser-safe subset of a store (/api/reference/storefront/get-store) {/* Generated from the public API spec by make gen. Do not edit. */} # List a store's pages, optionally filtered by slug (/api/reference/storefront/list-pages) {/* Generated from the public API spec by make gen. Do not edit. */} # Report a store to Sorai (/api/reference/storefront/report-store) {/* Generated from the public API spec by make gen. Do not edit. */} # Resolve a hostname or store id to its store (/api/reference/storefront/resolve-host) {/* Generated from the public API spec by make gen. Do not edit. */} # Record one tracking event (page view, checkout step, custom) (/api/reference/tracking/ingest-event) {/* Generated from the public API spec by make gen. Do not edit. */} # Webhooks (/api/webhooks) Sorai can send your server a request each time something happens to an order or a buyer: an order is paid, fulfilled or refunded, a new buyer checks out, access is granted or taken away. This page covers setting up an endpoint, the payload, verifying the signature, retries and every event. ## Set up an endpoint [#set-up-an-endpoint] In the Sorai console, open **Integrations**, then **Webhooks**, and add your endpoint's URL. Sorai creates a signing secret that starts with `whsec_` and shows it to you **once**. Store it somewhere safe. A lost secret cannot be shown again or rotated in place: delete the endpoint and add a new one. An endpoint receives every event unless you pick a list of events for it, when you add it or later from its settings. ## Payload [#payload] Every delivery is a `POST` with a JSON body of this shape: ```json { "version": "2026-08-01", "event": "order.paid", "id": "018f1b1a-0000-7000-8000-000000000000", "created_at": "2025-01-01T00:00:00Z", "data": { "order_id": "ord_123", "order_number": "SR-ABCDEFGHIJ", "store_id": "store_456", "status": "paid", "currency": "IDR", "total_minor": 150000, "subtotal_minor": 150000, "discount_minor": 0, "discount_code": "", "buyer": { "id": "buyer_789", "email": "buyer@example.com", "name": "Rina" }, "items": [ { "product_id": "prod_1", "price_id": "price_1", "name": "Ebook", "kind": "primary", "quantity": 1, "unit_amount_minor": 150000, "amount_minor": 150000 } ], "custom_fields": { "game_id": "12345" }, "payment": { "method": "qris", "amount_minor": 151500, "fee_minor": 1500 } } } ``` | Field | Type | Meaning | | ------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------- | | `version` | string | The version of this envelope. It changes only if the envelope's shape changes, not with every release. | | `event` | string | The event's name. See [Events](#events). | | `id` | string | A UUID for this event. Every retry of the same event carries the same `id`, so it is the key to deduplicate on. | | `created_at` | string | When Sorai recorded the event, as an RFC 3339 timestamp. | | `data` | object | The event's own fields, which depend on `event`. Always enough to identify the order, buyer or entitlement without another call. | Every money field is an integer in the currency's minor unit, never a float. ## Verify the signature [#verify-the-signature] Every delivery carries three headers: | Header | Meaning | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `X-Sorai-Signature` | `t=,v1=`. See below. | | `X-Sorai-Event` | The same value as the body's `event`, so you can route a request without parsing it. | | `X-Sorai-Delivery` | A UUID for this delivery attempt. Not the body's `id`: an event can be delivered more than once, and each attempt has its own. | The signature is: ``` signed_payload = "{timestamp}.{raw_request_body}" signature = hex(HMAC-SHA256(webhook_secret, signed_payload)) ``` `timestamp` is the `t=` value from the header. `raw_request_body` is the exact bytes of the body as they arrived. Verify against the raw body your framework gives you, before any JSON parsing: parsing and re-encoding can reorder keys or change whitespace, and the signature will no longer match. Check two things: 1. **The MAC.** Compute it and compare it with `v1` in constant time: `hmac.Equal` in Go, `hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node. Never `==`, which leaks timing an attacker can use to forge a signature byte by byte. 2. **The timestamp's age.** Reject a request whose `t` is too far from now. The timestamp is part of what is signed, so a captured request cannot be replayed later by changing an unsigned header. Five minutes is a good default. ### Test vector [#test-vector] Use this to check your implementation. **Secret:** ``` whsec_5f3a2b1c4d5e6f708192a3b4c5d6e7f8 ``` **Timestamp:** `1735689600` (2025-01-01T00:00:00Z) **Raw request body** (the exact bytes, on one line): ```text {"version":"2026-08-01","event":"order.paid","id":"018f1b1a-0000-7000-8000-000000000000","created_at":"2025-01-01T00:00:00Z","data":{"order_id":"ord_123","order_number":"SR-ABCDEFGHIJ","total_minor":150000,"currency":"IDR"}} ``` **Expected `X-Sorai-Signature`:** ``` t=1735689600,v1=c33ffe12039b04e193486c8f8b5fd6dd15bfdcf5a8bdd565270c5f2c5ba348dc ``` Reproduce it in Python: ```python import hmac, hashlib secret = b"whsec_5f3a2b1c4d5e6f708192a3b4c5d6e7f8" ts = "1735689600" body = b'{"version":"2026-08-01","event":"order.paid","id":"018f1b1a-0000-7000-8000-000000000000","created_at":"2025-01-01T00:00:00Z","data":{"order_id":"ord_123","order_number":"SR-ABCDEFGHIJ","total_minor":150000,"currency":"IDR"}}' signed_payload = ts.encode() + b"." + body signature = hmac.new(secret, signed_payload, hashlib.sha256).hexdigest() print(f"t={ts},v1={signature}") ``` ### Verifier in Node [#verifier-in-node] ```js import { createHmac, timingSafeEqual } from "node:crypto"; // rawBody is the request body as a Buffer, before any JSON parsing. export function verifySoraiSignature(secret, header, rawBody, toleranceSeconds = 300) { const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2))); const timestamp = Number(parts.t); if (!Number.isInteger(timestamp) || !parts.v1) return false; if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false; const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest(); const received = Buffer.from(parts.v1, "hex"); return received.length === expected.length && timingSafeEqual(received, expected); } ``` ### Verifier in Go [#verifier-in-go] ```go package webhooks import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "strconv" "strings" "time" ) // body is the request body exactly as it arrived, before any JSON parsing. func VerifySoraiSignature(secret, header string, body []byte, now time.Time, tolerance time.Duration) error { var ts int64 var sig []byte for _, part := range strings.Split(header, ",") { key, value, _ := strings.Cut(part, "=") switch key { case "t": ts, _ = strconv.ParseInt(value, 10, 64) case "v1": sig, _ = hex.DecodeString(value) } } if age := now.Sub(time.Unix(ts, 0)); age > tolerance || age < -tolerance { return errors.New("timestamp outside tolerance") } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(strconv.FormatInt(ts, 10) + ".")) mac.Write(body) if !hmac.Equal(sig, mac.Sum(nil)) { return errors.New("signature mismatch") } return nil } ``` ## Retries [#retries] Answer with any `2xx` status to accept a delivery. Anything else, a timeout or a connection error counts as a failure, and Sorai tries again. A delivery gets **8 attempts** in all, each after this delay: | Attempt | Delay before it | Time since the event | | ------- | --------------- | -------------------- | | 1 | 15 seconds | 15s | | 2 | 1 minute | 1m 15s | | 3 | 5 minutes | 6m 15s | | 4 | 15 minutes | 21m 15s | | 5 | 1 hour | about 1h 21m | | 6 | 4 hours | about 5h 21m | | 7 | 12 hours | about 17h 21m | | 8 | 24 hours | about 41h 21m | When the 8th attempt fails too, the delivery is marked `failed` and Sorai stops. You can send it again from the console: open the endpoint's deliveries and choose **Replay**. A replay starts the same 8 attempts again. **Endpoints that keep failing are turned off.** After **50 deliveries in a row** have each failed all 8 attempts, the endpoint is disabled and stops receiving events. One successful delivery resets the count, so an endpoint that is down now and then is never disabled; only an unbroken run of 50 failures does it. Fix the cause, then turn the endpoint back on in the console. **Deduplicate on `id`.** An event can arrive more than once, after a retry or a replay. Process each `id` once, however many requests carry it. ## Events [#events] Sorai sends nine events. For one successful order, they arrive in this order: | Event | Sent when | | --------------------- | ------------------------------------------------------------------------------------------------------------- | | `order.created` | An order is placed, before it is paid. | | `buyer.created` | An email checks out from your org for the first time. A repeat purchase from the same email does not send it. | | `entitlement.granted` | Payment settles and the buyer's access is recorded, once per entitlement. Download links do not exist yet. | | `order.paid` | Payment for the order settles. | | `order.fulfilled` | Access is granted and everything the buyer bought is ready to use. | | `order.expired` | An unpaid order's time to pay runs out. | | `order.cancelled` | You cancel an unpaid order from the console. | | `order.refunded` | A paid or fulfilled order is refunded. It is sent once per refund. | | `entitlement.revoked` | Right after `order.refunded`, once for each entitlement the refund took away. | An endpoint can only subscribe to these names. Asking for any other name is refused with `422 invalid_event`, so an endpoint never looks set up while waiting for an event that will never come. An endpoint that subscribes to everything (the default) starts receiving a new event the day Sorai starts sending it, with no change on your side. If an event is ever retired, an endpoint subscribed to it keeps working: saving the endpoint drops the retired name from its list instead of failing. The one exception is an endpoint whose list would be left empty, which would mean "everything"; that save is refused with `422 invalid_event` instead. `webhook.test` is not an event you subscribe to. It is what the console's **Test** button sends; see [Test an endpoint](#test-an-endpoint). ### Order events [#order-events] `order.created`, `order.paid`, `order.expired`, `order.cancelled` and `order.refunded` carry: | Field | Type | Meaning | | ---------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `order_id` | string | The order's UUID. | | `order_number` | string | The number the buyer sees, such as `SR-ABCDEFGHIJ`. | | `store_id` | string | The UUID of the order's store. | | `store_name` | string | The store's name when Sorai started sending this order's events, usually when it was placed. Absent on older orders. | | `store_slug` | string | The store's slug, as in its link. Absent when `store_name` is. | | `status` | string | The status this event reports: `awaiting_payment`, `paid`, `expired`, `cancelled` or `refunded`. On `order.created` for a free order it is `pending`, because there is no payment to await. | | `currency` | string | An ISO 4217 code. | | `total_minor` | integer | The order's total: `subtotal_minor - discount_minor`. Does not include a payment fee the buyer paid on top (see `payment`). | | `subtotal_minor` | integer | The sum of every line's `amount_minor`, before the discount. | | `discount_minor` | integer | What the discount code took off; `0` without one. | | `discount_code` | string | The code the buyer used; `""` without one. | | `buyer` | object | Who placed the order: `id`, `email`, `name`, and `phone` and `country` when the checkout collected them. On every order, not only a buyer's first. | | `items` | array | One entry per line. See below. | | `custom_fields` | object | The buyer's answers to your checkout form, keyed by each field's key; `{}` when it asked nothing. Each answer is at most 1000 characters. A renamed field keeps its old key on older orders. | | `attribution` | object | The `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content` and `referrer` the buyer arrived with; `{}` when none. The buyer can edit these. | | `metadata` | object | What your server set on the checkout session the order came from; `{}` otherwise. The buyer cannot change it. | | `checkout_session_id` | string | Optional. The checkout session's id. Absent for an ordinary checkout. | | `originating_order_id` | string | Optional. Set on an order bought from a post-payment offer: the order that offer was shown after. That order's `metadata` is copied here too, so check this before acting on it twice. | Each entry in `items`: | Field | Type | Meaning | | ------------------- | ------- | -------------------------------------------------------- | | `product_id` | string | The product's UUID. | | `price_id` | string | The price's UUID. Two variants of a product differ here. | | `name` | string | The line's name as the buyer saw it at checkout. | | `kind` | string | `primary`, or `bump` for an add-on accepted at checkout. | | `quantity` | integer | Units bought. | | `unit_amount_minor` | integer | The price of one unit. | | `amount_minor` | integer | `unit_amount_minor` times `quantity`. | `order.paid` also carries `payment`, read when the order was paid. The buyer may have switched payment method since `order.created`. | Field | Type | Meaning | | ---------------------- | ------- | ---------------------------------------------------------------------------- | | `payment.method` | string | How the buyer paid, such as `qris`, `va_bca`, `card` or `free`. | | `payment.amount_minor` | integer | What the buyer was charged, the payment fee included. | | `payment.fee_minor` | integer | The payment fee the buyer paid on top of `total_minor`; `0` when you pay it. | Every field is fixed when the order's events begin, and a retry never re-reads the order. A retried `order.created` still says `awaiting_payment` after the buyer has paid, so it deduplicates against the first attempt instead of looking like a new event. An older order may arrive without some of the newer fields, so check each one on its own. ### `order.fulfilled` [#orderfulfilled] Everything above, with `status: "fulfilled"`, plus: | Field | Type | Meaning | | ----------------- | ---------------- | --------------------------------------------------------------------------------------------------------------- | | `entitlement_ids` | array of strings | Every entitlement the order granted, so you can provision access without another call. | | `payment` | object | As on `order.paid`. | | `license_keys` | array | One entry per license key the order created: `id`, `entitlement_id`, `product_id` and `last4`. Empty when none. | A webhook never carries the license key itself. A delivery is logged, retried for hours and may go to a plain `http` endpoint or a tool that keeps every run, and a key kept there would outlive a refund that revoked it. `last4` is enough to tell keys apart; fetch a full key from your server with a secret API key that has the `orders:read` scope. ### `buyer.created` [#buyercreated] | Field | Type | Meaning | | ---------------- | ------ | -------------------------------------------------------------------------- | | `buyer_id` | string | The buyer's UUID. | | `email` | string | The buyer's email. A buyer is one email per org. | | `name` | string | Can be empty if the checkout did not ask for one. | | `phone` | string | Optional. Absent, not empty, when the checkout did not collect it. | | `country` | string | Optional. ISO 3166-1 alpha-2. Absent when not collected. | | `store_id` | string | The store whose checkout the buyer first used. | | `first_order_id` | string | The order that created the buyer, so the event stands on its own in a CRM. | ### `entitlement.granted` [#entitlementgranted] Sent once per entitlement, not once per order, so you can provision access product by product and deduplicate on `entitlement_id`. | Field | Type | Meaning | | ---------------- | ------ | --------------------------------------------------------------------- | | `entitlement_id` | string | The entitlement's UUID. A retry carries the same one. | | `buyer_id` | string | Who owns it now. | | `order_id` | string | The order that granted it. | | `product_id` | string | The product. | | `kind` | string | The product's kind, such as `file`, `course`, `license` or `service`. | | `expires_at` | string | Optional. RFC 3339. Absent when access does not expire. | ### `entitlement.revoked` [#entitlementrevoked] The refund's counterpart: sent once for each entitlement a refund took away, so you can remove access the same way you granted it. | Field | Type | Meaning | | ---------------- | ------ | -------------------------------------------------------------------- | | `entitlement_id` | string | The same id the `entitlement.granted` event carried. | | `buyer_id` | string | Who owned it. | | `order_id` | string | The refunded order. | | `product_id` | string | The product. | | `kind` | string | The product's kind, as on `entitlement.granted`. | | `revoked_at` | string | RFC 3339. A retry reports the first attempt's time, never a new one. | A refunded buyer loses access at once: their download links stop working and every license key the order created is revoked. Only a refund revokes an entitlement, and it cannot be undone. ## Which event to listen to [#which-event-to-listen-to] - **The money arrived:** `order.paid`. Record the sale, tell your team, or start anything that only needs to know it is paid. - **The buyer can use what they bought:** `order.fulfilled`. Provision access in your own app. It comes after entitlements and license keys exist, and carries their ids. - **A new customer:** `buyer.created`, once per email per org. For every purchase, use `order.paid`, which carries `buyer` on every order. - **Take access away:** `order.refunded`, then one `entitlement.revoked` per entitlement. Every event is delivered at least once. Deduplicate on the envelope's `id`. ## Match a sale to your own user [#match-a-sale-to-your-own-user] To know which of your users bought, open the checkout from your server: create a checkout session with a secret API key that has the `checkout:write` scope, put your own id in its `metadata` (for example `{"user_id": "u_123"}`), and send the buyer to the session's `url`. The order's events then carry `metadata.user_id` and `checkout_session_id`. A session link takes one payment and expires after a day unless you set otherwise. Pay-what-you-want prices cannot be sold this way, and discount codes are off unless the session allows them. Only `metadata` is safe to act on. `custom_fields`, hidden fields included, come from the buyer or from a link they can edit. Use them for something like a campaign tag, never to decide whose account to credit or which invoice is paid. Whatever you match on, check `status`, `items` and `total_minor` for what was actually bought. ## Tag a sale with a campaign or source [#tag-a-sale-with-a-campaign-or-source] UTM parameters on the link a buyer arrives by come back in `attribution` without any setup. For a tag of your own, add a **Hidden** field to the store's checkout form, say `partner`, and add `?partner=acme` to a checkout or product page link (or pass `fields: { partner: "acme" }` to `Sorai.checkout()`). The buyer never sees it, and it comes back as `custom_fields.partner`. Both are for reporting. A buyer can edit the link, so neither should decide whose account to credit or which invoice is paid: use `metadata` for that. ## Test an endpoint [#test-an-endpoint] The **Test** button on an endpoint in the console sends one `webhook.test` event straight away, signed like a real delivery, and shows you the status your server answered with. Its `data` is a sample of a real event with example values: the endpoint's first subscribed event, or `order.paid` when it subscribes to everything. Use it to map fields in a tool such as n8n before your first sale. Route on `event` so a test never starts real work. A test is not retried and does not count toward the endpoint being turned off, so send as many as you like. ## Receive webhooks in n8n [#receive-webhooks-in-n8n] 1. Add a **Webhook** node with method `POST`, and under **Options** turn on **Raw Body**. The signature covers the exact bytes Sorai sent, and a re-encoded body will not verify. 2. Add a **Crypto** node: action **Hmac**, type **SHA256**, encoding **Hex**, and your `whsec_` secret as the secret. Its value is the `t=` number from `X-Sorai-Signature`, a `.`, then the raw body as text, as in the [test vector](#test-vector). 3. Add an **IF** node that compares the result with the `v1=` part of `X-Sorai-Signature`, and drops the request when they differ or when `t` is more than five minutes old. 4. Add a **Switch** on `{{ $json.body.event }}` with a branch for each event you handle, and ignore `webhook.test`. 5. Press **Test** on the endpoint in the console to send a sample, then map fields from it. The exact expressions depend on your n8n version. The rule they implement is the one in [Verify the signature](#verify-the-signature). # API keys (/help/account/api-keys) An API key lets software act for your organization: the [Sorai CLI](/cli), your own code calling the [API](/api), or [sorai.js](/sorai-js) on a site you host yourself. You don't need one to sell from the console. Open **Organization**, then **API keys**. Owners and admins can create and revoke keys. ## Publishable or secret [#publishable-or-secret] - **Publishable**: belongs to one store and is safe to put in a web page. Use it for sorai.js on your own site. - **Secret**: belongs to the whole organization and can do only what you allow it. Use it for the CLI and for your own server. Never put it in a web page. Secret keys need the Pro or Business plan. ## Create a key [#create-a-key] 1. Choose **Create key**. 2. Pick the **Kind**: **Publishable** or **Secret**. 3. Optionally give it a **Name**, so you can tell your keys apart later. 4. For a publishable key, pick its **Store**. For a secret key, tick the **Permissions** it needs, such as **View orders** or **Manage products, prices, media and discount codes**. 5. Choose **Create key**, copy the key, then choose **Done**. A secret key is shown once, right after you create it. If you lose it, revoke it and create a new one. A publishable key can be copied again from its row at any time. For which permissions each task needs, see the [CLI quickstart](/cli/quickstart) and the [API reference](/api). ## Revoke a key [#revoke-a-key] Choose the bin icon on the key's row, then **Revoke**. Anything using the key stops working immediately, and this can't be undone. The key named **storefront (auto)** is created automatically so your storefront works. Revoking it stops your storefront working until you set a new one. ## Keep track of your keys [#keep-track-of-your-keys] The list shows each key's name, kind, prefix, when it was created and when it was last used, and whether it's **Active** or **Revoked**. A key you don't recognise, or one nobody has used in a long time, is safest revoked. # Your account (/help/account) Two things in Sorai have settings of their own. Your **account** is you: your name, email, password and preferences, the same in every organization you belong to. Your **organization** is your business: it owns your stores, your team, your API keys and your plan. - Open your account from your name in the top bar, then **Account**. - Open your organization from **Organization** at the bottom of the sidebar. It has three tabs: **Members**, **API keys** and **Billing**. # Members and roles (/help/account/members) Members are the people who can sign in to your organization: a helper who answers buyers, a partner who builds pages. Each has a role that decides what they can reach. Members belong to the organization, so they work across all of its stores. Open **Organization**, then **Members**. The list shows everyone with access and every invitation that hasn't been accepted yet, marked **invited**. ## Roles [#roles] | What they can do | Owner | Admin | Staff | | -------------------------------------------- | ----- | ----- | ----- | | Products, pages and media | Yes | Yes | Yes | | Orders | Yes | Yes | Yes | | Analytics | Yes | Yes | Yes | | Affiliates: see them | Yes | Yes | Yes | | Affiliates: set rates, approve, mark payouts | Yes | Yes | No | | Domains, integrations and notifications | Yes | Yes | No | | Verification: submit and resubmit | Yes | Yes | No | | Members and API keys: manage | Yes | Yes | No | | Earnings: see the balance | Yes | Yes | No | | Earnings: bank account and withdrawals | Yes | No | No | | Plan and billing: change | Yes | No | No | Staff can see the member list but not change it. Only an owner can make someone else an owner. ## Invite someone [#invite-someone] You need to be an owner or admin. 1. Choose **Invite member**. 2. Enter their **Email**. 3. Pick a **Role**. It starts on **Staff**. 4. Choose **Send invite**. If they already have a Sorai account, they join straight away. Otherwise they join the next time they sign in with that address; until then they show as **invited**. They don't need to set up a business of their own. ## Remove someone or withdraw an invitation [#remove-someone-or-withdraw-an-invitation] Choose the bin icon on their row, then confirm. - Removing a member takes away their access to this organization immediately. - Withdrawing an invitation means they won't join when they sign in. You can invite them again later. ## How many people you can add [#how-many-people-you-can-add] Your plan sets the size of your team, counting members and open invitations: one person on Free, three on Pro, and no limit on Business. When the team is full, **Invite member** says **Your plan's team is full. Upgrade to add more people.** # Organizations (/help/account/organizations) An organization is your business in Sorai. It owns your stores, products and team, your API keys and your plan, and it's what Sorai pays out to. Most creators have one, made for them in [setup](/help/getting-started/set-up-your-store). You can belong to more than one: your own, and a team you were invited to. ## The Organization page [#the-organization-page] Choose **Organization** at the bottom of the sidebar. It has three tabs: - **Members**: who has access and what each person can do. See [Members and roles](/help/account/members). - **API keys**: keys for the CLI, the API and sorai.js. See [API keys](/help/account/api-keys). - **Billing**: your plan and its payments. Everyone can see it; only owners can change the plan. ## Switch organizations [#switch-organizations] Use the organization picker at the left of the top bar, or choose **Switch** beside an organization on your [account page](/help/account/profile-and-preferences#your-organizations). On a phone, tap the name at the top to open the picker. ## Create another organization [#create-another-organization] A separate organization keeps its stores, team, plan and payouts apart from your first one. 1. In the organization picker, choose **New organization**. 2. Fill in: - **Name**: your business name. - **Short name**: how Sorai refers to this organization. Lowercase letters, numbers and hyphens, and it has to be unique. - **Country**: where this business is based. It decides the currency you're paid in. - **Default currency**: what Sorai can pay you in. Each store picks its own currency when you create it. - **Referral code (optional)**: if someone referred you to Sorai and the field is shown. 3. Choose **Create organization**. You're its owner, and you can invite people once it exists. An organization's country can't be changed later. If the short name is in use, the console says **That short name is already taken.** Pick another. # Profile and preferences (/help/account/profile-and-preferences) Your account page holds what is yours across every organization you belong to. Open it from your name in the top bar, then **Account**. ## Change your name, email or password [#change-your-name-email-or-password] 1. Under **Profile**, choose **Password and security**. 2. A secure page opens. Make your changes there. You come back to the console when you're done. Two-step verification is on the same page. ## Language and appearance [#language-and-appearance] Under **Preferences**: - **Language**: the console's own language, English or Bahasa Indonesia. It doesn't change what buyers see; each store has its own language in [Store settings](/help/pages/store-settings). - **Appearance**: **System**, **Light** or **Dark**. Both are saved on the device you set them on, so set them again on a new phone or computer. You can also change them from your name in the top bar. ## Your organizations [#your-organizations] **Organizations** lists every organization you're a member of and your role in each. The one you're working in is marked **Current**. Choose **Switch** to move to another. See [Organizations](/help/account/organizations). ## Cookie choices [#cookie-choices] Sorai uses cookies to measure how the console is used and which ads bring people to Sorai. Where the law asks for your permission first, the console asks before setting any of them. Elsewhere they're on until you turn them off. To change your choice, choose your name in the top bar, then **Cookie settings**: - **Necessary**: keeps you signed in and remembers your choice. Always on. - **Analytics**: measures how Sorai is used. - **Ads**: measures which ads bring people to Sorai. Choose **Save choices**. Your choice is saved to your account, so it follows you when you sign in on another device. These are Sorai's own cookies in the console. They're separate from the tracking you set up for your own store; see [Tracking](/help/selling/tracking). # Affiliates (/help/affiliates) An affiliate is someone who promotes your store and earns a share of each sale they send you. They share a link, or a discount code tied to them, and when a buyer pays, the sale is credited to them at the rate you agreed. There are three sides to this in Sorai: - **Your store's program.** You open a program, approve or invite affiliates, and pay them what they earn. You find it under **Affiliates** in the sidebar. It needs the Pro or Business plan. - **Promoting someone else's shop.** You join another creator's program and earn on the sales you send them. Your programs are under **My affiliate links** in your account menu. - **Bringing creators to Sorai.** You invite other creators to sign up and earn a share of what Sorai charges them. That is under **Refer creators** in your account menu. ## How the money moves [#how-the-money-moves] For affiliates of a store, Sorai keeps the account of what is owed; it does not move the money. Commission is held for 30 days after each sale, in case the buyer is refunded, then becomes payable. The store pays its affiliates directly, outside Sorai, and records each payment in the console so both sides see the same account. ## In this section [#in-this-section] # Manage your affiliates (/help/affiliates/manage-affiliates) Everyone who promotes your store is listed on the store's **Affiliates** page, with their status and their rate. Owners and admins manage them; staff can see the list and each affiliate's ledger. An affiliate is in one of these states: - **Applied**: waiting for you to decide. - **Active**: earning on the sales they send. - **Suspended**: earning nothing on new sales. - **Rejected**: you turned down their application. ## Approve or reject applications [#approve-or-reject-applications] New applications from your join page appear under **Waiting on you**, with the date they applied and the rate they get if you approve. - Choose **Approve** to let them in. Their link starts working straight away, at the rate shown. - Choose **Reject** to turn them down. They are told nothing automatically, and they cannot apply to your store again from the join page. ## Invite an affiliate [#invite-an-affiliate] Invite someone you already work with, without waiting for an application. 1. Choose **Invite an affiliate**. 2. Enter their **Email**: the address on their Sorai account. 3. Optionally, set their **Commission**. Leave it blank for your program rate. 4. Optionally, set their **Referral code**: letters, numbers, hyphens and underscores, up to 64 characters. Leave it blank and Sorai generates one. 5. Choose **Add affiliate**. They join straight away; there is nothing for you to approve afterwards. The person needs a Sorai account first. Owners and admins of your store cannot be its affiliates. ## Change one affiliate's rate [#change-one-affiliates-rate] 1. In the list, open the actions menu on their row. 2. Choose **Set their rate**, enter the new rate, and choose **Save**. The new rate applies to sales from now on. Commission already earned keeps the rate it was earned at. ## Suspend or reinstate [#suspend-or-reinstate] Choose **Suspend** from the actions menu on their row, then confirm. They stop earning on new sales and their link stops crediting them. Commission already on their ledger is untouched, including anything still held. To let them earn again, choose **Reinstate** from the same menu. If an affiliate's row says **Account disabled, earns nothing**, their Sorai account itself is disabled. That is not something you can change. ## See one affiliate in detail [#see-one-affiliate-in-detail] Choose an affiliate in the list to open their page. It shows their referral link, when they joined, **What you owe** them, their **Ledger** of every commission, reversal and payment, and the **Payments you have recorded**. From there you can **Record a payment**; see [Pay your affiliates](/help/affiliates/pay-affiliates). ## Troubleshooting [#troubleshooting] Ask them to sign up first, then invite them. Look for them in the list. Pick a different referral code, or leave it blank. # My affiliate links (/help/affiliates/my-affiliate-links) You can be an affiliate of other creators' shops on Sorai, and earn a share of each sale you send them. Your programs belong to you as a person, not to your organization, so they follow you whichever organization you have selected. Open your account menu in the top bar and choose **My affiliate links**. The page is **Your affiliate programs**. ## Join a shop's program [#join-a-shops-program] 1. Open the shop's affiliate page. It is usually **/affiliate** on the shop's address, and the shop may send you the link. 2. Read the rate and the terms, then choose **Sign in to apply**. 3. Sign in with your Sorai account. The application is sent, and the shop appears under **Your affiliate programs**. The shop reviews every application. While it decides, the card says it is waiting on the shop, and your link starts working the moment they approve you. A shop can also invite you directly, using the email on your Sorai account; then you join straight away. You cannot be an affiliate of a shop you run. ## Share your link [#share-your-link] Each program card shows the rate you earn, your referral link with a copy button, and your code. - A visitor who arrives on your link is credited to you for 30 days. If they later arrive on someone else's link, the newer one wins. - A discount code tied to you credits the sale to you, whichever link the buyer came in on. The card also shows **Clicks** on your link and what is **Payable now**. ## Follow what you have earned [#follow-what-you-have-earned] Choose **Your commission** on a program card to open its page: - **Sales you referred**: completed sales, per currency. A sale counts once the buyer has paid. - **What you have earned**: your balance per currency, as **Payable now**, **Held**, **Paid** and **All time**. - **Every entry**: each commission, reversal and payment, as it happens. - **Payments recorded**: each payment the shop recorded after sending it. ## How you get paid [#how-you-get-paid] Commission is held for 30 days after the sale, in case the buyer is refunded, and becomes payable after that. The shop pays you directly, outside Sorai, and records each payment here. Sorai never holds or moves this money, so agree with the shop how and when they pay. You earn in the currency each sale was taken in. Sorai never converts between currencies, so balances are kept apart. If a refund comes in after you were paid, your balance can go negative for a while. The next commission clears it; nothing is owed back to the shop. ## Troubleshooting [#troubleshooting] The shop has no open program. Your application is already in. Find the shop under **Your affiliate programs**. Nothing earns on any program while your account is disabled. # Pay your affiliates (/help/affiliates/pay-affiliates) You pay your affiliates yourself, directly, by whatever means you agree with them. Sorai does not move this money and does not take it out of your balance. What Sorai does is keep the account: what each affiliate earned, what is still held, what is payable, and what you say you paid. ## How commission is held [#how-commission-is-held] Each commission is held for 30 days after the sale, in case the buyer is refunded. After that it becomes payable. - A refund inside the 30 days cancels the commission. - A refund after it is taken back too, as a **Reversal** on the ledger. If you had already paid that commission, the balance can go negative for a while; the next commission clears it. Your affiliate is never asked to send money back. ## See what you owe [#see-what-you-owe] Open **Affiliates**, then choose the affiliate. **What you owe** shows a card per currency, because balances in different currencies are kept apart: - **Payable now**: what you can pay them today. - **Held**: commission still inside its 30 days. - **Paid**: what you have recorded as paid. - **All time**: everything they have earned. The **Ledger** below lists every entry: a **Commission** for each sale, a **Reversal** for each refund, and a **Payment** for each payment you recorded. Each commission is marked **Held** (with the date it becomes payable), **Payable** or **Paid**. Choose **View the order** to open the sale behind it. ## Record a payment [#record-a-payment] Pay your affiliate first, outside Sorai. Then record it, so you both see the same account. 1. On the affiliate's page, choose **Record a payment**. 2. Pick the **Currency**. The dialog shows how much is payable now. 3. Enter the **Amount**, up to what is payable. Less than that is a partial payment, and the rest stays payable. 4. Optionally, add **Your reference**, such as a transfer number or a receipt, so you can find the payment again, and a **Note**. 5. Choose **Record payment**. The payment appears under **Payments you have recorded**, and your affiliate sees it on their side. Recording a payment is a note of what you say you paid. Sorai does not check that the money arrived. You can only record what is payable. Commission still inside its 30 days cannot be recorded as paid yet. ## Troubleshooting [#troubleshooting] No commission has finished its 30 days. Come back once it is payable. The commission in that currency is still held. Pick another currency, or wait. A refund landed after you paid. The next commission clears it. # Refer creators (/help/affiliates/refer-creators) When another creator signs up to Sorai with your invite link, you earn a share of what Sorai charges them, for a set number of months. It costs them nothing. The console shows the current rate and the number of months before you start. This is separate from a store's affiliate program: you are referring a creator to Sorai, not a buyer to a shop. Your referral link belongs to you as a person, so it stays the same whichever organization you have selected. ## Get your invite link [#get-your-invite-link] 1. Open your account menu in the top bar and choose **Refer creators**. 2. Read the rate and the number of months on offer, then choose **Get my invite link**. **Your invite link** appears with a copy button, and your **Code** under it. Send the link to a creator. If they sign up with it, the share is yours. They can also type your code in when they create their organization. ## Follow your referrals [#follow-your-referrals] **Creators you referred** lists everyone who signed up with your link, with the date they joined and how long you earn on them: - **Earning**, with **You earn until** and a date: your share is still running. - **Finished**: the months have passed, and you no longer earn on them. Each referral keeps the rate it was made on. If the rate changes later, the row shows the rate that applies to that creator. **Your share** shows a card per currency, with what you have **Earned**, what was **Taken back by refunds**, what is **Owed to you now**, and what is **Already paid**. A refund on a creator's sale takes your share of it back. Sorai doesn't charge creators these fees yet, so nothing has been paid out and there is no date to expect a payment on. Your share is recorded as it is earned, and it will be paid once charging starts. ## Troubleshooting [#troubleshooting] You cannot take a referral link right now, and a link you already have earns nothing: anyone who signs up with it now credits nobody. What you have already earned is still shown. Get in touch with Sorai if this is a surprise. Creators who sign up with your link show up here once they do. # Run an affiliate program (/help/affiliates/run-a-program) An affiliate program lets other people promote your store for a commission on each sale they send. Each store has its own program, with one rate that it offers new affiliates. Affiliate programs are part of the Pro and Business plans. On Free, the console offers the upgrade when you try to open one; see [Plans and billing](/help/money/plans-and-billing). Owners and admins run the program. Staff can see the affiliates and their ledgers but cannot change anything. ## Open your program [#open-your-program] 1. Select the store, then open **Affiliates** in the sidebar, under **Sales**. 2. At the top, **Your affiliate offer** shows **Closed** while there is no program. Choose **Open a program**. 3. Enter the **Commission** as a percentage. 4. Choose **Save**. The page says **Your program is open to applications**. ## How commission is worked out [#how-commission-is-worked-out] Commission is a share of the sale, after discounts and before tax. It is counted in the currency the buyer paid in, and balances in different currencies are kept apart. A sale is credited to an affiliate in two ways: - **Their link.** A buyer who arrives on an affiliate's link is credited to them for 30 days. If the buyer later arrives on someone else's link, the newer one wins. - **Their discount code.** A discount code tied to an affiliate credits the sale to them, whichever link the buyer arrived on. You choose who a code credits when you set it up; see [Discount codes](/help/products/discount-codes). A sale counts once the buyer has paid. ## Share your join page [#share-your-join-page] Your store has a join page at **/affiliate** on its address. It shows your rate and the terms, and a **Sign in to apply** button for applicants. They need a Sorai account to apply. On **Your affiliate offer**, copy the join link, or choose **Open join page** to see it as an applicant does. Share it wherever your audience is. Every application waits for you to approve it; see [Manage your affiliates](/help/affiliates/manage-affiliates). You can also invite someone directly. ## Change the rate or close the program [#change-the-rate-or-close-the-program] Choose **Change rate** on **Your affiliate offer**, enter the new **Commission**, and choose **Save**. The new rate is what the join page advertises and what each new affiliate starts on. It never changes the rate of anyone who has already joined, and never changes commission already earned. To change an existing affiliate's rate, see [Manage your affiliates](/help/affiliates/manage-affiliates). To close the program, set the rate to 0. The join page then has nothing to offer, and the page says **Your program is closed**. # Create your account (/help/getting-started/create-your-account) Your Sorai account is you: your name, your email and your password. Your business, its stores and its team come next, in [setup](/help/getting-started/set-up-your-store). Starting is free, and you can upgrade any time. ## Sign up [#sign-up] 1. Open [console.sorai.ai/signup](https://console.sorai.ai/signup). 2. Choose **Sign up with Google** or **Sign up with email**. 3. Finish on the sign-in page that opens. With email, you choose your password there. You come back to the console and setup starts straight away. By signing up, you agree to Sorai's Terms and Privacy Policy; both are linked under the buttons. Already have an account? Choose **Sign in** at the bottom of the page. ## If you picked a plan first [#if-you-picked-a-plan-first] If you chose a paid plan on the Sorai pricing page before signing up, the sign-up page shows it in one line: the plan, how often it bills and its price. You don't pay yet. You pay once your store is set up, and you can still start free at that point. See [Set up your store](/help/getting-started/set-up-your-store#activate-your-plan). ## If someone invited you [#if-someone-invited-you] When an owner or admin invites your email address to their team, you don't go through setup. Sign up or sign in with that same address and you join their organization with the role they gave you. See [Members and roles](/help/account/members). ## An email asks you to confirm an address [#an-email-asks-you-to-confirm-an-address] A store can send its alerts to more than one email address. When someone adds yours, Sorai emails you a link to confirm it. You don't need a Sorai account for this. 1. Open the link in the email. 2. Choose **Confirm address**. Once you see **Address confirmed**, store alerts arrive at that address. If the page says **This link doesn't work**, it was used already or a newer link replaced it. If it says **This link has expired**, ask the person who added your address to send a new one. # Dashboard (/help/getting-started/dashboard) The dashboard is the first screen you see for a store. While the store is new, it shows what's left before your first sale. Once you're selling, it shows how the store is doing over the last 30 days. ## Finish setting up your store [#finish-setting-up-your-store] Until every item is done, **Finish setting up your store** sits at the top with a progress bar. Each item links to the screen that finishes it. - **Complete your store**: add your first product, then set up your store's page. Your store starts with a draft homepage; it counts once you've edited or published it. - **Verify your identity before selling**: shown when verification applies to your account. Buyers can pay, and pages can be published, once your identity is verified. Under it you see where your submission stands, such as **Verification pending**. The checklist disappears once everything on it is done. The first time you open a new store's dashboard, a short tour walks through the checklist, **Products** and **Pages**. Choose **Skip tour** to close it. Replay it any time with the help icon on the checklist. ## Before your first sale [#before-your-first-sale] Your store is ready for buyers when: 1. You have a product with a price. See [Create a product](/help/products/create-a-product). 2. Your homepage is published. See [Page builder](/help/pages/page-builder). 3. Your verification is approved, where it applies. See [Verification](/help/money/verification). The **Live** badge in the sidebar tells you when buyers can reach your store. See [Find your way around](/help/getting-started/find-your-way-around#is-your-shop-live). ## The cards [#the-cards] - **Revenue by currency**: paid orders, totalled one figure per currency. Sorai never converts between currencies. - **Funnel**: **Views**, **Checkouts** and **Purchases**, with the number of visitors. - **Top products**: revenue by product, in your store's currency. Sales in other currencies are listed when you hover a bar. - **Bump and offer performance**: how often buyers add your order bump and accept your post-payment offer. Shown only when your store has one set up, or had one. See [Checkout](/help/selling/checkout). - **Top pages**: views, purchases and revenue by page. - **Top sources**: traffic that arrived with UTM parameters, by source. Visits without them count as **(direct)**. See [Tracking](/help/selling/tracking). - **Recent orders**: your latest orders. See [Orders](/help/orders). - **Realtime, last 60 min**: live visitors and activity in your store, on Pro and Business. On Free, this space shows **More on Pro** instead. A new store shows empty cards, such as **No traffic yet** and **No orders yet**. They fill in as buyers visit and buy. ## How far back the cards go [#how-far-back-the-cards-go] Revenue and the funnel always cover the last 30 days. On Free, the other cards cover the last 7 days, and each card says so under its title, with **See your full history** to upgrade. Your store records everything either way, so upgrading shows the full history right away. # Find your way around (/help/getting-started/find-your-way-around) Every screen in the console has the same frame: a bar across the top that says which business and store you're in, a sidebar of everything in that store, and, when something needs you, a banner above the page. ## The top bar [#the-top-bar] - **Organization and store**: two pickers, side by side. The first is your organization (your business), the second is the store inside it. Pick a different one to switch. Each list ends with **New organization** or **New store**. On a phone the two fold into one button that opens both lists. - **Your name**: opens your menu, with **Account**, **My affiliate links**, **Refer creators**, **Appearance**, **Language** and **Sign out**. Where the console asks about cookies, **Cookie settings** is here too. ## The sidebar [#the-sidebar] The sidebar lists the current store's screens in groups: - **Store**: **View shop** opens your storefront in a new tab, and **Dashboard**. - **Catalog**: **Products**, **Pages**, **Media**. - **Sales**: **Orders**, **Checkout**, **Affiliates**, and **Earnings** for the whole organization. - At the bottom: **Store settings** for this store, and **Organization** for your team, API keys and plan. On a wide screen you can collapse the sidebar to icons to give a page more room. On a phone, open it with the menu button at the left of the top bar. ## Is your shop live? [#is-your-shop-live] Beside the **Store** heading, a badge says whether buyers can reach your store: - **Live**: your store is active and its homepage is published. - **Not live**: buyers who open your store's address get Page not found. When the only thing missing is your homepage, **View shop** first opens **Your homepage isn't live**. Product pages still open. Choose **Open and publish** to publish the homepage, **Choose a homepage** if your store has none set, or **Open anyway** to look at the store as it is. If publishing waits on verification, the dialog offers **Complete verification** instead. See [Page builder](/help/pages/page-builder). ## Banners [#banners] Some banners stay at the top of every page until the thing they ask for is done. - **Verification**: owners and admins see where verification stands, from **Verify your business to start selling** to **Your verification is in review**. Selling and payouts stay locked until it is approved. See [Verification](/help/money/verification). - **Suspension**: **This account is suspended** or **This store is suspended**, with the reason when there is one. Contact Sorai support for help. - **Payment due** or **Past due** beside **Organization** in the sidebar means your plan's payment needs attention. It opens billing. ## Tours [#tours] The first time you open some screens, a short tour points out what matters there. On your [dashboard](/help/getting-started/dashboard), replay it with the help icon on the setup checklist. On **Pages** and in the page builder, choose the help button, then **Take the tour**. # Getting started (/help/getting-started) Sorai runs from the console at [console.sorai.ai](https://console.sorai.ai). You sign up, answer three short questions about your business, and land on a store that is ready for its first product. These pages take you from there to a store buyers can open. # Set up your store (/help/getting-started/set-up-your-store) The first time you sign in, the console asks three questions, one per screen: your business, where it is based, and your store. Nothing is created until the last step, so you can go back and change an answer until then. If you leave partway, the console picks up where you left off the next time you open it in the same browser. ## 1. Your business [#1-your-business] Enter the **Business name** you sell under, then choose **Continue**. If someone referred you to Sorai and the screen shows **I have a referral code**, choose it and enter their code. If you followed a referral link, the code is already filled in. A code Sorai doesn't recognise is ignored. ## 2. Where you are [#2-where-you-are] - **Country**: where your business is based. Payouts and verification follow this country. - **Currency**: what your prices and orders use. Sorai pays out in a few currencies for now, so your country's own currency may not be listed. If it isn't, pick one Sorai can pay you in. Your country can't be changed after setup. Check it before you create your store. ## 3. Your store [#3-your-store] - **Store name**: what buyers see. - **Store address**: the first part of your store's web address. Use lowercase letters, numbers and hyphens only. You can connect your own domain later; see [Custom domains](/help/pages/custom-domains). - **Store language**: the language buyers see on your storefront and at checkout. Choose **Create store**. If the address is taken, the console says **That address is taken. Try another.** Pick another and create again. Your store uses the currency you chose in step 2. By creating your store, you agree to Sorai's Terms and Privacy Policy. ## Activate your plan [#activate-your-plan] This step appears only if you picked a paid plan before signing up. Your store already exists at this point. Pay now to start on that plan, or choose **Not now, start free** and activate it later from **Organization**, then **Billing**. If you leave a payment unfinished, the dashboard shows **Continue payment**. Your store runs on Free until the payment lands. ## What happens next [#what-happens-next] You land on your store's [dashboard](/help/getting-started/dashboard). Your store starts with a draft homepage, and a short tour points at the three things between you and your first sale: a product, your page and, where it applies, verification. - [Create a product](/help/products/create-a-product) - [Build your page](/help/pages/page-builder) - [Verify your business](/help/money/verification) # Stores (/help/getting-started/stores) A store is where your products, pages and payments live. Each one has its own address, currency and language, and its own products, orders and settings. Your organization can hold more than one: a store per brand, say, or per language. Your team and your earnings belong to the organization and are shared across its stores. ## Switch stores [#switch-stores] Use the store picker in the top bar, beside your organization's name. Pick a store and the console opens its dashboard. On a phone, tap the name at the top to open the picker. ## Create another store [#create-another-store] 1. In the store picker, choose **New store**. 2. Fill in: - **Name**: what buyers see. - **Slug**: becomes the store's web address. Lowercase letters, numbers and hyphens only. - **Currency**: what this store's prices and orders use, from the currencies your organization can be paid in. - **Language**: what buyers see on this store's storefront and checkout. 3. Choose **Create store**. If the slug is in use, the console says **That slug is already taken.** Pick another. ## How many stores you can have [#how-many-stores-you-can-have] Your plan sets how many stores your organization can have: one on Free, three on Pro and ten on Business. To add more, upgrade from **Organization**, then **Billing**. ## An organization with no store [#an-organization-with-no-store] If you joined someone's organization before it had a store, you see **No stores yet** instead of a dashboard. Choose **Create store** to make the first one. # Help Center (/help) Sorai is where creators sell digital products: ebooks, courses, templates and anything else a buyer downloads or opens online. This Help Center walks through the [console](https://console.sorai.ai), where you run your store. Building on Sorai from code? The [developer docs](/) cover the CLI, sorai-js, the API and MCP. ## Start here [#start-here] # Earnings and payouts (/help/money/earnings-and-payouts) When a buyer pays you, the money lands in your balance. It is yours. You move it to your bank yourself, with a withdrawal, to a bank account you have added. Nothing is sent to your bank automatically. Open **Earnings** in the sidebar, under **Sales**. Earnings belong to the whole organization, so the page is the same whichever store you have selected. Owners and admins can see this page. Only the owner can add bank accounts and withdraw; everyone else sees **Only the owner can withdraw money.** ## Before you can withdraw [#before-you-can-withdraw] Your business has to be verified first; see [Verify your business](/help/money/verification). Right after verification, the page may say **Your payout account is still being set up**. Sales keep landing in your balance in the meantime, and withdrawing opens as soon as the setup finishes. ## Read your balance [#read-your-balance] **Your balance** shows one card per currency. Sorai never converts between currencies, so a sale in one currency stays in that currency's card. Each card has three figures, and they are never added together: - **Pending balance**: money from recent sales that is still in its holding period. Once that passes, it moves to your current balance. - **Current balance**: money you can withdraw to your bank account now. - **Withdrawn balance**: money you have withdrawn that is on its way to your bank and has not arrived yet. Under the cards, **Read** shows when the figures were last read. Your first sale shows up here; until then the page says **Nothing earned yet**. ## Add a bank account [#add-a-bank-account] A withdrawal can only go to an account listed under **Payout accounts**. 1. Choose **Add bank account**. 2. Pick the **Country** and the **Currency**. A withdrawal to this account has to be in this currency. 3. Enter the **Account holder** exactly as your bank has it. 4. Choose the **Bank** and enter the **Account number**. 5. Choose **Add account**. Your bank then checks the account against the name you entered. The account shows **Being checked** until it passes, then **Verified**. If it shows **Rejected**, your bank did not accept it: choose **Remove account**, then add it again with the details exactly as your bank has them. Sorai does not store your account number. Only its last digits are shown. ## Withdraw money [#withdraw-money] 1. Choose **Withdraw money**. 2. Under **To**, choose the account. 3. Enter the **Amount**, or choose **Withdraw all**. The amount available is shown under the field. 4. Choose **Continue**, check the amount and the account, and choose **Withdraw** with the amount. The money leaves your current balance as soon as you confirm. You can have one withdrawal at a time; start the next one once this one arrives. ## Follow a withdrawal [#follow-a-withdrawal] The page updates on its own. A withdrawal moves through these states, and only the third means the money is in your account: 1. **Withdrawal requested**: your bank has not received it yet. 2. **On its way to your bank**: your bank has accepted the transfer and is moving it. 3. **The money arrived**: it is in your account. If it ends in **The withdrawal failed**, the money did not arrive and is back in your current balance. The reason is shown under **Why it failed**. Check the account, then try again. Two other states mean you should wait rather than try again: - **The payments team is handling this**: it is taking longer than usual. The amount stays set aside until it is paid or back in your balance. - **We couldn't confirm this withdrawal**: it may still be on its way. Sorai is checking, and the page updates on its own. **Past withdrawals** lists your most recent withdrawals and where each went. ## Troubleshooting [#troubleshooting] Your sales are still in their holding period, in **Pending balance**. They move to your current balance once it passes. The account is still **Being checked**. You can withdraw to it once it passes. Add an account in the same currency as the balance, and wait for it to be checked. Check the name and the account number against a bank statement, then add the account again. Split the money into smaller withdrawals, or contact support. A withdrawal to it is still on its way. Remove it once the money has arrived. Past withdrawals keep naming a removed account, and you can add it again later. # Money (/help/money) Two kinds of money move through the console, and they never mix: - **Your earnings.** What buyers pay for your products lands in your balance on the **Earnings** page. It is yours, and you withdraw it to a bank account you add. - **What you pay Sorai.** Your plan and any AI credits you buy are paid from **Organization**, then **Billing**. Each of those payments has a receipt. Before any money can leave, your business has to pass verification. Until then a banner across the top of the console says **Verify your business to start selling**, and selling and payouts stay locked. ## Who can do what [#who-can-do-what] - **Verification**: owners and admins submit it. Staff can see its status. - **Earnings**: owners and admins see the balance. Only the owner adds bank accounts and withdraws money. - **Billing**: only the owner changes the plan or pays. Every member can see the payment history and open receipts. Roles are set on the **Members** tab; see [Members](/help/account/members). ## In this section [#in-this-section] # Plans and billing (/help/money/plans-and-billing) Your plan is what you pay Sorai. It is separate from your earnings: paying for a plan never touches your balance. Every organization starts on **Free**, and you can move to **Pro** or **Business** at any time. Open **Organization** at the foot of the sidebar, then the **Billing** tab. Only the owner can choose, pay for, change or cancel a plan. Everyone else can see the plan and the payment history. ## What each plan unlocks [#what-each-plan-unlocks] The **Choose a plan** cards show each plan's current price and what it adds. **Pro** adds: - Your own domain - Order bumps and upsells - Affiliates - Webhooks and API **Business** adds: - The lowest fees - Up to 10 storefronts - Unlimited team members - Dedicated support When you reach for something your plan does not include, the console names the plan it needs and offers **See plans**, which brings you here. ## Start a plan [#start-a-plan] 1. Under **Choose a plan**, pick **Monthly** or **Yearly**. Yearly is marked **Save 20%**. 2. Choose **Choose Pro** or **Choose Business**. 3. Check the plan and the amount, pick how to pay under **Pay with**, and choose **Continue to payment**. 4. Pay with the method you picked: - **QRIS**: scan the code with your bank or e-wallet app. - **Virtual account**: transfer the amount to the virtual account number shown, from your bank. - **E-wallet**: finish the payment in the app, then come back. - **Card**: enter your card details and choose **Pay by card**. The plan starts once the payment lands. You can leave the page while you wait; when it lands you see **You're on** with your plan. A payment code expires if it is not paid in time; choose **Get a new code** to finish. The methods listed depend on what is available for your organization and the amount. ## Pay automatically by card [#pay-automatically-by-card] With **Card, autopay** your card is charged each billing period without you doing anything. Otherwise each period brings a bill to pay by hand. When your subscription is late, the **Organization** row in the sidebar shows **Past due**; while a new subscription waits for its first payment, it shows **Payment due**. Either one opens billing. On an active plan, the **Current plan** card offers: - **Switch to card**, to start paying automatically. - **Replace card**, to use a different card. - **Switch to paying by hand**, which removes your card. If your card is declined, pay that bill another way. A new card is used for later charges. ## Change your plan [#change-your-plan] On the plan you want, choose **Switch to** with its name. - **Moving up** (to Business, or from monthly to yearly) happens once you pay. The confirmation shows a **Credit for unused** time on your current plan, the amount **Due now**, and the **New period**. - **Moving down** is scheduled for the end of your current period. You stay on your current plan until then. Choose **Schedule change** to confirm. To keep your current plan instead, choose **Cancel change**. While a plan change waits for payment, you cannot start another one or cancel. Pay for it, or let it expire. ## Cancel [#cancel] Choose **Cancel subscription** on the **Current plan** card. Your plan runs until the end of the period you paid for, and the card shows **Ends on** with that date. After that the plan shows **Ended**. If the subscription was never paid, cancelling removes it straight away. You can subscribe again at any time. ## AI credits [#ai-credits] The **AI credits** card shows how many credits you have left of your plan's monthly allowance, plus any top-up. Monthly credits are used first. To buy more: 1. Choose **Buy AI credits**. 2. Pick an amount, or one of the **Quick picks**. The **Total** updates as you choose. 3. Pick how to pay and finish the payment as for a plan. The credits are added once the payment lands, and expire 12 months after payment. If a top-up is not paid in time, nothing is charged and no credits are added. ## Plans provided by Sorai [#plans-provided-by-sorai] If Sorai has given your organization a plan, the card says **Provided by Sorai**, with an end date if there is one. You can still choose a larger plan. ## Troubleshooting [#troubleshooting] Plans cannot be bought from the console right now. Contact Sorai to upgrade. Choose another way to pay. Wait a few minutes, then try again. Ask the owner to check on this instead. # Payment history and receipts (/help/money/receipts) These are the payments your organization made to Sorai: plan payments, renewals, plan changes and AI credit top-ups. Money your buyers paid you is on the **Earnings** page instead; see [Earnings and payouts](/help/money/earnings-and-payouts). Every member of the organization can see the history and open receipts. ## Find a payment [#find-a-payment] Open **Organization**, then the **Billing** tab, and scroll to **Payment history**. Each row shows the **Date**, what it was **For** (for example "Pro, renewal"), the **Period** it covers and the **Amount**. A payment shows up here as soon as it lands. ## Open a receipt [#open-a-receipt] 1. In **Payment history**, choose **View** on the payment. 2. The receipt is issued by Sorai and **Billed to** your organization. It shows when it was **Paid on**, what it was **For**, the period, how it was **Paid with**, and the **Amount**. 3. Choose **Print** to print it or save it as a PDF from your browser's print dialog. Choose **Back to billing** to return. Each receipt carries a **Reference** you can quote to Sorai support. A receipt is a record of payment, not a tax invoice. ## Troubleshooting [#troubleshooting] The payment is not in your organization's history. Check that you have the right organization selected, or open **Billing** and choose the payment from the list. The renewal did not state an amount, so the receipt shows the plan's listed price instead. # Verify your business (/help/money/verification) Verification is a one-time check on who is behind your organization. Until it is approved, selling and payouts stay locked: buyers cannot pay you, and no money can leave your balance. Do it once per organization, before your first sale. The owner or an admin submits it. Staff can see where it stands but cannot send or change it. ## Where to start [#where-to-start] A banner across the top of every console page tells you what verification needs next. Choose **Verify now** on it. If you saved answers earlier without sending them, the banner says **Finish your verification** and the button is **Continue**. Once you are verified, a **Verified** badge sits beside the title of the **Earnings** page. Choose it to see your verification details. ## Before you begin [#before-you-begin] Have these ready: - An official identity document, as a clear photo or scan. - The bank account you want to be paid to, with the account holder name exactly as the bank has it. What exactly is asked for depends on your country and on whether you sell as an individual or a business. The form only shows the fields that apply to you. ## Fill in the form [#fill-in-the-form] The form has six steps. Choose **Next** to move on; your answers are saved as you go, and nothing is sent until the last step. 1. **Country and entity.** Your country is shown under **Where you're based**, with the currency you are paid in. It was set when your organization was created and cannot be changed. Then answer **Who is this account for?**: **An individual** if you sell under your own name, or **A registered business** if you sell under a registered company. 2. **What you sell.** Pick the **Category** that best describes your products. Choosing one confirms you have read the seller policy. Sorai is for digital products only, and a category marked **Not accepted** cannot be chosen. 3. **Identity.** Enter your details exactly as they appear on your official document. Phone numbers take a country code; a leading 0 on the number is fine. 4. **Documents.** Choose **Upload document** for each file asked for. Make sure all four corners are visible, the text is sharp enough to read, and no glare or shadow covers the details. The accepted formats are listed on the step. 5. **Bank account.** Choose your **Bank** and enter the **Account number** and **Account holder name**. Use the name exactly as it is registered with the bank: a mismatched name is the most common reason a payout account is rejected. 6. **Review.** Check every answer, then choose **Submit for verification**. If you leave partway, the form picks up where you left off next time. ## After you submit [#after-you-submit] Checking usually takes 1-2 working days, and you get an email either way. The **Verification** page shows one of these: - **Verification in progress**: your details are being reviewed. There is nothing to do but wait. - **More information needed**: some answers need changing or adding. The steps that need work are marked **Needs attention**, with the reason beside each field. Fix them and choose **Resubmit**. - **Verification rejected**: the last submission was not approved. Correct the form and choose **Resubmit**. - **Verified**: payments and publishing are open for your organization. Verification approves your business. Your payouts still go to the bank accounts you add on the **Earnings** page; see [Earnings and payouts](/help/money/earnings-and-payouts). ## Troubleshooting [#troubleshooting] Sorai is not yet taking sellers, or cannot yet pay out, in your organization's country. The country cannot be changed on an existing organization. To sell from another country, create a new organization. Something changed between two submit attempts. Choose **Submit for verification** once more. Type in the search box and choose a match from the list rather than typing the name freely. Your role does not include verification. Ask an owner or admin to do it. # Orders (/help/orders) Every purchase from your store becomes an order, newest at the top. An order starts when a buyer begins checkout, so the list also shows orders that were never paid. Open **Orders** in the sidebar, under **Sales**. The page has two tabs: - **All orders**: every order, with its status. - **Unpaid checkouts**: checkouts that ended without payment, and how each one is being won back. See [Unpaid checkouts](/help/selling/abandoned-checkouts). ## Read the list [#read-the-list] Each row shows the order **Number**, the **Buyer**'s email, the status, the **Total** and when it was **Created**. On a phone the row folds into two columns, with the money on the right. Select an order number to open the order; see [Order details](/help/orders/order-details). ## Order statuses [#order-statuses] - **Pending**: checkout has started. The buyer has not picked a payment method yet. - **Awaiting payment**: the buyer picked a method. Sorai is waiting for the money to arrive. - **Paid**: the payment arrived. - **Fulfilled**: paid, and what the buyer bought has been delivered to them. - **Expired**: the buyer didn't pay before the payment window closed. Nothing was charged. - **Cancelled**: the order was cancelled before it was paid, by the buyer or by you. - **Refunded**: the money went back to the buyer and their access was taken back. ## Filter and search [#filter-and-search] Above the list: - **Search**: an order number or a buyer's email. - **Status**: one status, or **Any status**. - **Currency**: one currency, or **Any currency**. - **From** and **Until**: a date range for when the order was created. If nothing matches, widen the date range, or clear the status and currency. ## Export orders [#export-orders] **Export** downloads a CSV file of the orders that match your current filters. Exporting depends on your plan. If you see **This export stopped early**, there were more matching orders than one file can hold. Narrow the filters, for example to a shorter date range, and export again for the rest. # Order details (/help/orders/order-details) Open an order from **Orders** to see everything about it on one page. At the top are the order number, which you can copy, its status, and a short summary of what happened last. Some statuses carry a line that explains them. ## Timeline [#timeline] **Timeline** lists what happened to the order, oldest first: when it was created, paid, delivered, cancelled, expired or refunded, and each payment event. Each entry has a reference you can copy if you need to ask support about it. ## Items [#items] **Items** lists what was bought: the **Name**, the **Kind**, the quantity and the **Amount**. An item added from an order bump shows the kind **Order bump**. Below the items are the **Subtotal**, any **Discount**, the **Service fee** if the buyer paid one, and the **Total**. An order bought from a post-payment offer links back to the order it followed, and that order links forward to the offer order. ## Payment [#payment] **Payment** shows the **Method**, its **Status**, the **Payment reference**, what was **Charged**, any **Tax included**, and the amount that went **To your balance**. If the buyer paid in a different currency from the one you are paid in, **Settled** shows the sale in your currency. For a virtual account it also shows the **Bank** and **Account number**. While the order is unpaid, **Payment window closes** shows when the buyer's time to pay runs out. If a payment attempt was refused but the order is still open, a note says so: the buyer can still pay until the window closes. ### Needs attention [#needs-attention] An order marked **Needs attention** has a payment that settled but could not be applied to it. Nothing else happens automatically. Send Sorai support the order number, and Sorai matches the payment to the order for you. ## Buyer [#buyer] **Buyer** shows the buyer's **Name**, **Email**, **Phone** and **Country**, with copy buttons for the email and phone. **Buyer answers** shows what the buyer filled in for your own checkout questions. See [Set up checkout](/help/selling/checkout). ## What the buyer received [#what-the-buyer-received] - **Delivered** lists the files the buyer got, with how many **Downloads left** and when each **Link expires**. The buyer's own download links are not shown here, because following one would spend a download they paid for. - **What the buyer owns** lists each product the order gave them and whether it is still active. - **License keys** appears when the order includes a license product. Keys stay hidden until you reveal them. ### Mark a service delivered [#mark-a-service-delivered] For a service, the **Delivered** column reads **Not yet** until you mark it. Once you have performed the service, choose **Mark delivered** and confirm. This records today's date. The buyer isn't notified, and the date can't be changed. ## Where the sale came from [#where-the-sale-came-from] - **Affiliate** shows who earned on this sale and their **Commission**. See [Affiliates](/help/affiliates). - **Attribution** shows where the buyer came from, captured on their first landing: the campaign tags (**Source**, **Medium**, **Campaign**), the **Referrer**, the landing page, and ad click ids from Meta and TikTok. ## Act on the order [#act-on-the-order] Depending on its status, the order page offers **Cancel order** or **Refund**. See [Refunds and cancellations](/help/orders/refunds-and-cancellations). These buttons appear only if your role lets you manage orders. # Refunds and cancellations (/help/orders/refunds-and-cancellations) What you can do depends on the order's status: - **Pending** or **Awaiting payment**: you can cancel it. - **Paid** or **Fulfilled**: you can refund it. - Anything else: neither. An expired or cancelled order was never charged. Both buttons sit at the top of the order page, and appear only if your role lets you manage orders. ## Cancel an unpaid order [#cancel-an-unpaid-order] 1. Open the order and choose **Cancel order**. 2. Confirm with **Cancel order**. You see **Cancellation requested**. If the buyer's payment lands at the same moment, the payment wins and the order goes ahead. ## Refund an order [#refund-an-order] A refund is always for the full amount. The buyer loses their downloads and any license key on the order, and it can't be undone. 1. Open the order and choose **Refund**. 2. Check the amount in the dialog. 3. Optionally, add a **Reason (optional)**. It is for your records; the buyer never sees it. 4. Choose **Refund order**. If the buyer paid a service fee, the dialog says whether the refund includes it. The buyer gets an email confirming the refund and listing what access it took back. ### How the money goes back [#how-the-money-goes-back] The dialog tells you which way this payment is refunded: - **Refunded to the original payment method.** The money goes back the way the buyer paid. - By bank transfer: some payment methods can't be refunded by the provider. Sorai emails the buyer a link to enter their bank details, then sends the refund by bank transfer. This takes a few days. The original payment fee isn't returned and a bank transfer fee applies; both come out of your balance. Some payment methods can't be refunded through Sorai at all. On those orders the page says so instead of offering **Refund**, and you arrange the refund with your buyer directly. **Store settings**, then **Payment methods** shows which methods these are; see [Payment methods](/help/selling/payment-methods). ### Follow a refund by bank transfer [#follow-a-refund-by-bank-transfer] The order's **Payment** card shows **Refund by bank transfer** with its status: - **Waiting for the buyer's bank details**: the buyer hasn't used the link yet. **Resend link** emails them a fresh one. - **Checking the bank details**, **Waiting to send**, **Sending**: on its way. - **With the payments team**: the refund is being reviewed before it is sent. No action is needed from you. - **Sent**: the transfer has gone out. - **Transfer failed**: your buyer is still owed the refund. The card says why and what to do. Often that is **Retry transfer**; if your own details aren't verified yet, finish [verification](/help/money/verification) first, then retry. ## Orders with nothing to pay [#orders-with-nothing-to-pay] For a free order, the button reads **Revoke access**. The buyer loses their downloads and any license key on the order. Nothing was charged, so nothing goes back. This can't be undone. ## If a refund doesn't go through [#if-a-refund-doesnt-go-through] Reload the page to see its current status. Try again from the order page. Retrying can't refund twice, so don't refund by hand until you have. Refund the buyer in the payment provider's own dashboard. # What buyers receive (/help/orders/what-buyers-receive) Sorai emails your buyers at each step of an order, on your store's behalf. Knowing what they got helps when a buyer writes to you. Each email names your store and the order number, and points the buyer to you for help. ## After they pay [#after-they-pay] The buyer gets a receipt with the subject "Your order (number) from (your store)". It shows the order summary and what they bought: - **Files**: a download link for each one. If the files are still being prepared, the email says they will appear in the buyer's library within a few minutes. - **License keys**: the keys the order issued. - **Services**: a link to book a session, when the product has a booking link. For a free order, the same email confirms the order is complete. ## Their library [#their-library] Every purchase made with the same email address stays in the buyer's library, so they can come back for a file from any device. The receipt links to it. To sign in again later, the buyer asks for a sign-in link, which Sorai emails to them. The link works once and expires. Download links have a limited number of uses. When a buyer uses the last one for a file, Sorai emails them to say so. On the order page, **Delivered** shows how many downloads each file has left; see [Order details](/help/orders/order-details). ## Before they pay [#before-they-pay] - **Order created, waiting for payment**: once the buyer picks a payment method, they get the payment instructions by email, such as the virtual account number to transfer to. - **Reminder**: while the order is still open, Sorai can remind the buyer how long they have left to pay. - **Expired**: if the payment window closes first, the buyer is told the order expired and they weren't charged. Buyers who leave a checkout unpaid also get a recovery email with a link back. See [Unpaid checkouts](/help/selling/abandoned-checkouts). ## After a refund [#after-a-refund] The buyer gets an email confirming the refund, with the amount and what it took back: download access, license keys or access to a service. When the refund goes by bank transfer, they first get an email with a link to enter their bank details, then another when the transfer is sent or if it fails. See [Refunds and cancellations](/help/orders/refunds-and-cancellations). # AI assistant (/help/pages/ai-assistant) The AI assistant writes whole-page drafts: the first one, and every change you ask for after it. It reads the page, your products, your images and your theme, then writes a new draft for you to look at. It never publishes. Nothing goes live until you do. It only edits the page you have open. It does not change products, prices or settings. ## Start a new page with AI [#start-a-new-page-with-ai] On **Pages**, choose **Build with AI**, describe the page, and choose **Build it**. [Create a page](/help/pages/create-a-page) has the steps. The new page opens in the builder while the assistant works, and when it finishes the panel says **Your page is ready**. The result is a draft: edit anything, ask for more changes, and publish when you are happy with it. ## Ask for a change [#ask-for-a-change] 1. In the builder, choose **Ask AI**. 2. Under **What should the agent change?**, describe the change, or pick one of the suggestions. Name the section and what it should say; specific instructions get better results. 3. Choose **Generate**. While it works, the panel lists what it is doing, step by step, and the builder pauses editing on this draft. You can close the panel; the run carries on, and the **Ask AI** button shows that it is still working. Choose **Stop** to end it early. You are only charged for what had already run. ## Review what it wrote [#review-what-it-wrote] When a run finishes, the preview already shows the assistant's version and the panel lists what it changed: blocks added, edited, removed or moved. - **Apply to the builder** makes it your draft. Every earlier version stays in the page's history. - **Discard** puts your version back. Then ask for the next change, or choose **New conversation** to start over. ## Credits [#credits] Each run uses AI credits from your organization's monthly allowance. The panel shows how many you have left. Credits reset at the start of each month. ## When a run does not work [#when-a-run-does-not-work] - **Your AI credits are used up.** Your organization has spent its monthly credits. That attempt used none. They reset at the start of the month. - **This page already has a run going.** Only one run at a time can write a page's draft. Watch the one that is already going. - **Too many runs at once.** Your organization already has as many runs going as it can. Wait for one to finish. - **Nothing on the page changed** or **The agent did not do that.** The assistant only edits your page. Ask for a change to a section, its wording or its images. - **Nothing usable came back.** Your draft is untouched. A more specific instruction, naming which section and what it should say, usually fixes it. - **The AI service did not answer.** Nothing was changed. Give it a minute and start the run again. # Create a page (/help/pages/create-a-page) Open **Pages** in the sidebar. You have three ways to start a page. Whichever you pick, the new page opens in the builder as a draft, and nobody sees it until you publish. ## Build with AI [#build-with-ai] Describe the page and the AI assistant writes the first draft. 1. Choose **Build with AI**. 2. Under **Address**, set where the page lives on your storefront. You can change it later. 3. Describe the page: who it is for, what it sells, the tone you want. 4. Pick how you want to edit it: **Editable section by section** (blocks) or **One document you edit directly** (HTML). 5. Choose **Build it**. The builder opens while the assistant works. See [AI assistant](/help/pages/ai-assistant) for what happens next. ## Start from a template [#start-from-a-template] A template is a finished page with example copy. Every block stays editable. 1. Choose **Start from a template**. 2. Pick one: **Product launch**, **Online course**, **Digital download**, **Coming soon** or **Link in bio**. The preview shows it in your store's theme. 3. Choose **Use this template**. 4. Set the **Slug**, and optionally the **Product this page sells**. Every buy button points at that product from the start. 5. Choose **Create page**. You can leave the product for later and pick it in the builder. Publishing waits until every buy button points at a real product. ## Start from a blank page [#start-from-a-blank-page] 1. Choose **Blank page**. 2. Enter the **Slug**: the page's address on your storefront. The dialog shows the address it will be reachable at. 3. Under **How you'll build it**, pick **Blocks** or **HTML**. 4. Under **What it's for**, pick a kind. A block page starts with a few starter blocks for that kind; an HTML page starts empty whichever you pick. 5. Choose **Create page**. The kinds are: - **Landing**: a campaign or sales page, built to convert one visit. - **Product**: sells one product. Match the slug to a product to take over its URL. - **Home**: laid out like a front page. You choose the homepage on the pages list, not here. - **Custom**: anything else, such as a policy or thank-you page. Starts empty. Starter blocks are ordinary blocks. Rewrite, reorder or delete any of them. ## If the slug is refused [#if-the-slug-is-refused] Another page in this store uses it. Pick another. A few addresses belong to the shop itself, such as `checkout` and `library`, and cannot be used as a page slug. ## The pages list [#the-pages-list] Each row shows the page's slug, kind, mode, status and when it was last updated. A published page has a link that opens it on your storefront; a draft shows **Publish to get a link**. A warning mark next to the status means the page has **Unpublished changes**: the latest edits are saved as a draft and are not live yet. To remove a page, choose the delete icon on its row, then **Delete page**. The page stops answering on the storefront. Its version history is kept. Your homepage cannot be deleted; choose another homepage first. New to pages? **How pages work** on the pages list opens a short practice run. Nothing in it is saved or shown to buyers. # Add a custom domain (/help/pages/custom-domains) A custom domain lets buyers reach your storefront and checkout on your own hostname, such as `shop.example.com`, instead of your store's Sorai address. You add the domain in the console, add its DNS records where you manage your domain, and Sorai takes it from there. Custom domains need the Pro or Business plan. On a plan without them, **Add domain** offers the upgrade instead. ## Add the domain [#add-the-domain] 1. Open **Store settings**, then **Domains**. 2. Choose **Add domain**. 3. Enter the **Hostname**, for example `shop.example.com`. Enter the name only, with no `https://`, no path and no port. 4. Choose **Add domain**. The console then opens the setup for that domain. ## Add the DNS records [#add-the-dns-records] The setup shows where your domain's DNS is managed, under **DNS is managed at**. Sorai recognises Cloudflare, GoDaddy, Namecheap and Hostinger and marks the one it found as **Detected**. If that is wrong, or yours isn't listed, pick another or **Another provider**. Under it are two short steps for that provider, then the records to add. Names are written the way your provider's DNS page expects them, and each name and value has a copy button. **Open DNS at** links straight to your provider's DNS page. | Type | Name | Value | | ----- | ------------------------------------------ | ------------------------------------------ | | CNAME | your hostname | `cname.sorai.ai`, as shown in the console | | TXT | `_sorai-verify.` followed by your hostname | the verification code shown in the console | - The **CNAME** record points your domain at Sorai, so visitors reach your store. - The **TXT** record proves the domain is yours. Its value is unique to this domain. With **Another provider**, a name may show its **Full name** as well. Use the short one if your provider adds your domain to the end of every name, and the full one if it doesn't. ### Cloudflare [#cloudflare] The CNAME note tells you how to set **Proxy status**: either that proxy on or off both work, or to set it to **DNS only**. Follow the note the console shows. ### A bare domain [#a-bare-domain] A bare domain is one without a part in front, like `example.com`. Most providers don't allow a CNAME there, so the console asks you to use the `www` name instead and forward the bare domain to it. When the console shows **A** (and **AAAA**) records for a bare domain instead, add those in place of the CNAME. ## Follow the setup [#follow-the-setup] A strip at the top of the setup shows where the domain is: **Add records**, **Verify**, **Certificate**, then **Live**. Each record shows **checking**, **found** or **not found yet**. If the CNAME points somewhere else, the record names where it points now. Sorai checks on its own every few minutes, so you can close the setup and come back. After you add the records, choose **Check now** to have Sorai look at once. DNS changes can take a while to spread. Once the records are found, the domain is verified and its security certificate is issued, which usually takes a few minutes. If the certificate is still pending after that, the setup shows a **TXT (certificate)** record; add it as well. When the certificate is issued, the domain serves your store. To open the setup again later, choose **DNS records** on the domain's row (**Manage** once it is live). ## Domain status [#domain-status] | Status | What it means | | ------------ | --------------------------------------------------------- | | **pending** | Waiting to find the records. | | **verified** | The domain is yours; its certificate is being issued. | | **active** | The domain is serving your store. | | **failed** | The setup did not finish in time and checks have stopped. | The **TLS** column shows the certificate: **Active**, **Issuing**, **On first visit** (issued when the first visitor arrives), **Not available** or **Failed**. ## Keep the records in place [#keep-the-records-in-place] Sorai rechecks every live domain daily. If the records are gone, the row shows **Records missing** and the setup names a date. Put the records back by then, or the domain stops serving your store and you have to add it again. ## If something goes wrong [#if-something-goes-wrong] Enter the name only, like `shop.example.com`, with at least one dot. Another store already uses it, so it can't be added here. The record shows where your domain points now. Change the CNAME's value to the one in the console, or remove the old record that points elsewhere. The setup shows the reason. Add the **TXT (certificate)** record if it is shown, then choose **Check now**. The setup did not finish in time, or the records stayed missing past their date. Remove the domain and add it again to retry, then check every record carefully. ## Remove a domain [#remove-a-domain] 1. Choose **DNS records** or **Manage** on the domain's row. 2. Choose **Remove domain**, then confirm. The domain stops serving your store immediately. If you add it again later, it goes through verification and a new certificate first. # Pages and your shop (/help/pages) Your shop is a set of pages. A page is what a buyer lands on: your homepage, a sales page for one product, a link-in-bio page, a thank-you note. You build them in the console under **Pages**, and nothing a buyer sees changes until you publish. Every store starts with one page, `/home`, set as its homepage. It starts as an unpublished draft, so your shop is not live until you publish it. ## Live or not live [#live-or-not-live] The **Store** heading in the console sidebar carries a badge: - **Live** means your homepage is published and buyers who open your store's address see it. - **Not live** means buyers who open your store's address get Page not found. Product pages still open. Select the badge to go to **Pages** and fix it. [Publish your shop](/help/pages/publish-your-shop) walks through it. ## Drafts and publishing [#drafts-and-publishing] Every change you make in the builder is saved as a draft as you go. The live page stays exactly as it was until you choose **Publish**. Every earlier version stays in the page's history, so you can look back or restore one. ## Two ways to build a page [#two-ways-to-build-a-page] - **Blocks**: build it visually from ready-made sections such as a hero, a pricing table, an FAQ or a checkout form. - **HTML**: write your own markup, CSS and JavaScript. ## In this section [#in-this-section] # The page builder (/help/pages/page-builder) Open any page from **Pages** to edit it in the builder. The block list sits on the left, a live preview of your storefront in the middle, and the settings for whatever you select on the right. On a narrow screen the side panels open from the **Blocks** and **Settings** buttons instead. Your work saves as a draft while you type. The status next to the page's name says where it stands, for example **Draft saved** or **Unsaved changes**. Nothing goes live until you [publish](/help/pages/publish-your-shop). ## Add and arrange blocks [#add-and-arrange-blocks] 1. Choose **Add a block** and pick one from the list. 2. Drag a block in the list to move it. With a keyboard, focus the block's handle, press space or enter, move it with the arrow keys, and press space or enter again to drop it. 3. Use the icons on a block to duplicate or remove it. The blocks you can add: | Block | What it is | | ------------------ | ------------------------------------------------ | | **Hero** | Headline, subtitle and up to two buttons | | **Features** | A grid of short benefit cards | | **Pricing** | A product's prices, live from the catalog | | **Testimonials** | Quotes from buyers | | **FAQ** | Questions and answers | | **Gallery** | A carousel of images | | **Video** | One embedded video | | **Call to action** | A closing pitch and one button | | **Links** | A column of buttons, one per link | | **Product card** | One product with its buy button | | **Checkout** | The checkout form, in the page | | **Rich text** | Formatted text: headings, bold, italic and lists | | **Countdown** | A timer counting down to a date | | **HTML embed** | Your own markup, rendered as-is | | **Stats** | Three or four figures, each with a label | | **Steps** | Numbered steps, each a title and a line | **Pricing**, **Product card** and **Checkout** need a product from your catalog. If you have none yet, add one under **Products** first. See [Create a product](/help/products/create-a-product). ## Edit a block [#edit-a-block] Select a block in the list or in the preview. Its fields open on the **Content** tab. - **Buttons** can **Open checkout** for a product and price, **Go to a link** (a full address or another page such as `/another-page`), or **Scroll to a block** on the same page. - **Images** come from your store's media. Choose **Browse** to pick one, and **Crop** to choose what stays in frame. - Open **Appearance** to change this block only: its background (a colour, a gradient, an image or a brand tint), text colour, width, padding, alignment and shape. A block marked **needs attention** is missing something it needs. Until you fill it in, the draft cannot save, and the status reads **Fill in the highlighted block to save**. ## Page settings [#page-settings] The **Page** tab holds the page's **Slug**, **Kind**, **Title**, **Description**, **Social image** and **Canonical URL**, and whether to **Show store header & footer**. Block pages show the store header and footer by default, so buyers always have a way back to the shop. These settings are not part of the draft: choose **Save page settings** to save them. Changing the slug changes the page's address on your storefront. ## Preview [#preview] The preview shows the page as buyers will see it, in your theme. Switch between **Full width**, **Tablet** and **Mobile** to check each size, or open the preview in a new tab. ## Version history [#version-history] The **History** tab lists every saved version, marked by where it came from: your edits, a template, the AI builder, or a restore. To go back to one: 1. Choose **View** to show it in the preview. 2. Choose **Restore this version**. The version replaces your current draft. The live page stays as it is until you publish. To save a version you can find easily later, choose **Save as a version** from the page's more actions menu. **Discard draft**, in the same menu, puts the editor back to the published version. Your unpublished edits stay in the history. ## HTML pages [#html-pages] An HTML page has a **Code** tab with the page's HTML, CSS and JavaScript, and a **Preview** tab. **Import HTML** replaces the page with markup you paste: its styles and inline scripts move to the CSS and JavaScript tabs, and its title and description fill in the page settings. You can turn a block page into an HTML page with **Convert to HTML**. Converting to HTML cannot be undone. The page becomes code you edit, and the block editor is gone for it. Countdowns stop counting, galleries stop sliding, videos lose their player, and a checkout block becomes a buy button that opens checkout in its own window. Buy buttons keep working. Theme overrides apply to block pages only; an HTML page carries its own CSS. # Publish your shop (/help/pages/publish-your-shop) Editing a page only ever changes its draft. Publishing makes the draft the live version buyers see. Your shop is live once its homepage is published. ## Publish a page [#publish-a-page] 1. Open the page from **Pages**. 2. Choose **Publish**, then **Publish** again to confirm. The draft becomes the live version on your storefront straight away. Every earlier version stays in the page's history. The page's row on **Pages** now has a link that opens it on your storefront. Keep editing after that and the row shows **Unpublished changes** until you publish again. ## Why Publish is off [#why-publish-is-off] - **Publishing opens after verification.** While your business verification is not approved, no page can go live. The banner at the top of the console takes you to it. See [Verification](/help/money/verification). - **Before you publish.** A page made from a template still has a button pointing at the template's example product, or a link with no address. The list under this heading names each block; open it and point it somewhere real. - **Fill in the highlighted block to save.** A block is missing something it needs, so the draft cannot be saved or published. ## Your homepage [#your-homepage] The homepage is the page buyers see when they open your store's address. Every store starts with `/home` as its homepage. While the homepage is not published, the console says so: - The sidebar shows **Not live** next to **Store**. - **Pages** shows **Your homepage isn't live**, with **Open and publish** to take you straight to it. - **View shop** warns you before it opens the shop, because buyers who open your store's address get Page not found. Product pages still open. Publish the homepage and the badge turns to **Live**. ## Choose another homepage [#choose-another-homepage] Only a published page can be the homepage. 1. On **Pages**, find the published page you want. 2. Choose the house icon on its row (**Set as homepage**). 3. Confirm with **Set as homepage**. Buyers who open your store's address now see this page. Its own address still works too. The homepage stays published: you cannot unpublish or delete it. To take it down, make another page the homepage first. ## Unpublish a page [#unpublish-a-page] 1. Open the page. 2. In the more actions menu, choose **Unpublish**, then confirm. Buyers get Page not found for it until you publish again. Nothing is deleted. # Store settings (/help/pages/store-settings) **Store settings** holds everything that configures one store. Open it from the foot of the console sidebar. It has five sections: **General**, **Theme**, **Payment methods**, **Domains** and **Integrations**. With more than one store, each has its own settings; pick the store first in the store switcher. ## General [#general] - **Logo**: shown in your storefront header in place of the store name. Uploading or removing it saves straight away. - **Store name**: required. - **Language**: the language your storefront speaks to buyers. - **Default currency**: the store's currency, chosen from the currencies your shop sells in. - **Status**: **Active** or **Disabled**. Choose **Save** to keep your changes. A **Disabled** store stops answering: buyers who open any of its pages get Page not found. Set it back to **Active** to reopen it. ### Branding [#branding] **Hide "Powered by Sorai"** removes the Sorai line from your storefront footer. It needs the Pro or Business plan; on a plan without it, the option is marked with the plan it needs. ## Theme [#theme] Fonts, colours and corners for the whole shop. See [Theme and colours](/help/pages/theme). ## Payment methods [#payment-methods] ### Payment fee [#payment-fee] Under **Who pays**, choose who carries the fee for accepting a payment: - **I pay the payment fee**: the price stays what you set. - **My buyers pay it as a service fee**: the fee is added to what they are charged at checkout. Then choose **Save**. If the card says **Set by Sorai. Contact support to change it.**, the choice is fixed for your account. ### Which methods buyers can use [#which-methods-buyers-can-use] **Payment methods** lists what your buyers can pay with, grouped as **QRIS**, **Virtual Account**, **E-wallets** and **Card**. Turn each one on or off. Each method shows its fee, who pays it, its minimum and maximum amount, and whether it can be refunded. The list is empty until your account can take payments. A method marked **Not available in your currency** cannot be used in your store's currency. A method marked **Paused** cannot be chosen by buyers right now. One marked **No longer offered** cannot be used at all; turn it off. ## Domains [#domains] Serve your shop and checkout from a domain you own. See [Add a custom domain](/help/pages/custom-domains). ## Integrations [#integrations] **Tracking** holds this store's ad and analytics pixels; see [Tracking](/help/selling/tracking). **Webhooks** and **Notifications** are set for your whole organization rather than one store. ## Who can change settings [#who-can-change-settings] Members without permission to manage the store see these settings read-only. See [Members](/help/account/members). # Theme and colours (/help/pages/theme) The theme is how your shop looks: its fonts, colours and corners. You set it once for the store, and every page picks it up. A single page can override the store's theme, and a single block can override its page. ## Change the store theme [#change-the-store-theme] 1. Open **Store settings**, then **Theme**. 2. Pick a starting point under **Start from a preset**, or skip to the fields below. **Moods** set the fonts, spacing and section style as well as the colours; **Palettes** set the colours only. 3. Under **Type**, choose the fonts for **Headings** and **Body text**, the **Heading weight**, **Letter spacing**, **Heading size** and **Small labels** (the short line above a heading, like NEW). 4. Under **Colour and shape**, choose **Light** or **Dark**, the **Corner radius**, and the **Colours**. 5. Check the **Live preview**, then choose **Save theme**. Applying a preset saves it right away, and so does **Save theme**. Theme changes reach your live shop without publishing any page. ## The colours [#the-colours] Each colour is named for what it paints: | Colour | Where it shows | | ---------------------- | --------------------------------------- | | **Page background** | Behind the whole page | | **Card background** | Cards, panels and the checkout box | | **Section background** | Bands that sit apart from the page | | **Body text** | Headings and paragraphs | | **Secondary text** | Captions, help text and small print | | **Lines and borders** | Dividers, card outlines and input edges | | **Buttons and links** | Buy buttons, links and focus rings | | **Text on buttons** | The label inside a filled button | | **Highlights** | Eyebrows, badges and small flourishes | A colour you leave unset uses the default. ## Contrast warnings [#contrast-warnings] If two colours are too close to read, for example body text on the page background or the label on a buy button, the theme shows a warning such as **One colour pair may be hard to see**, with the pair and how far apart they are. It never stops you saving. Change either colour in the pair to clear it. ## Advanced [#advanced] **Advanced** lists colour names your shop no longer paints with. They were saved before the list above existed and are kept so nothing is lost. Removing one removes it for good. ## Override the theme on one page [#override-the-theme-on-one-page] In the builder, open the **Theme** tab. There you can set a **Font** (or **Use the store's font**), a **Corner radius** and **Colours** for this page only. A colour you leave on **Store's** follows the store theme. Page overrides are part of the page's draft, so they go live when you publish the page. They apply to block pages only; an HTML page carries its own CSS. ## Override one block [#override-one-block] Select a block and open **Appearance**. Its colours sit over the page's, and the page's over the store's. The builder marks a block whose colours may be hard to read. # Courses (/help/products/courses) A course is a set of lessons buyers work through inside Sorai. They sign in to watch on any device and continue from where they stopped. Buyers get every published lesson. You build it in the **Course content** card on the product. Lessons sit inside sections, and buyers see both in the order you set. ## Add sections and lessons [#add-sections-and-lessons] 1. Open the course product and find **Course content**. 2. Type a title in **New section title** and choose **Add section**. 3. Under the section, choose **Add lesson**. 4. Pick the lesson **Type**, give it a **Title**, and add its content. 5. Choose **Save lesson**. Use the move buttons to reorder sections and lessons. To change a lesson, choose its edit button. ## Lesson types [#lesson-types] - **Video**: upload a video, or paste a link. See below. - **File**: a file buyers download, such as a worksheet. - **Text**: a page you write in **Content**. Markdown works. **Length in minutes** is optional. It shows on the product page and is used to mark the lesson as watched. ### Upload a video or paste a link [#upload-a-video-or-paste-a-link] Under **Where the video comes from**, pick one: - **Upload a file**: stored with Sorai, and only buyers can open it. It counts against your storage. - **Paste a link**: a YouTube or Vimeo address. It uses none of your storage, but anyone holding the link can watch it, and Sorai cannot lock a video it does not host. An unlisted video works; a private one does not. Most buyers watch on a phone over mobile data. A 720p export looks as good there, uploads faster and starts playing sooner. The top of **Course content** shows how much storage the course uses and the largest file you can upload. Both depend on your plan. ## Publish lessons [#publish-lessons] A lesson buyers can see is **Published**. Turn off **Published** to keep a lesson hidden while you work on it; it shows a **Draft** badge. A course needs at least one published lesson before it can go live, or buyers would open it and find nothing to watch. Turn on **Free preview** to let anyone open a lesson from the product page without buying. It shows a **Preview** badge. ## Delete a section or lesson [#delete-a-section-or-lesson] Deleting a section deletes its lessons too. Deleting a lesson removes it from the course for buyers, along with any progress they recorded on it. ## See how far buyers get [#see-how-far-buyers-get] The **Engagement** card shows, for each lesson, how many buyers **Opened** it and how many **Completed** it, and how many buyers have opened at least one lesson. It fills in once a lesson is published and buyers start watching. Engagement needs permission to view orders. See [Members](/help/account/members). ## When a lesson shows a warning [#when-a-lesson-shows-a-warning] - **Upload processing**: the upload hasn't finished yet. - **Upload too large**: the file is over your plan's per-file limit. Upload a smaller one. - **Media missing**: the lesson has no file to play. Edit the lesson and upload one. - **We could not open that video**: check the link works and the video isn't set to private. # Create a product (/help/products/create-a-product) A new product starts as a draft, so nobody can buy it while you set it up. You create it in two short steps, then add its price and what the buyer gets on the product's own page. ## Create it [#create-it] 1. In the sidebar, under **Catalog**, open **Products**. 2. Choose **New product**. 3. Under **Product type**, pick what you're selling. Each type shows what **Your buyer gets** and what to do **After you create it**. Choose **Continue**. 4. Enter a **Name**. The **Slug** fills itself in from the name; it is the product's own address on your shop. Use lowercase letters, numbers and hyphens only. 5. Choose **Create product**. The product page opens. To pick a different type before creating it, choose **Change**. ## Fill in the product page [#fill-in-the-product-page] The product page is a stack of cards. Which ones you see depends on the type. - **Buy link**: the address of the product's page on your shop, to share or open. - **Details**: the **Name**, **Slug** and **Description**, plus the fields that belong to the type, such as **Note to buyers** for a license or **What happens next** for a service. Choose **Save** when you're done. - **Selling content**: how the product page sells itself. - **Prices**: what it costs. See [Prices and editions](/help/products/prices). - **Files**, **Course content** or **What's included**: what the buyer receives. See [Files and delivery](/help/products/files-and-delivery) and [Courses](/help/products/courses). - **Checkout offers**: what else this product's buyers are offered. See [Checkout offers](/help/products/offers). ### Selling content [#selling-content] These fields shape the product page buyers see. - **Cover image**: upload one, or choose **Choose from library** to reuse an image from [Media](/help/products/media). Images can be up to 20 MB. - **Formatted description**: shown on the product page, below the buy button. Headings, bold text and lists only. - **Gallery**: up to 5 images from your media library. Drag them to reorder. - **What you get**: short lines listing what the buyer receives. - **Testimonials**: a quote and a name for each. - **FAQ**: questions and their answers. ### Thank-you redirect [#thank-you-redirect] **Thank-you redirect** in **Details** sends buyers to a page of yours a few seconds after they pay, instead of leaving them on the receipt. They can always choose to stay on the receipt. The link must start with `https://`. Leave it empty to keep the receipt. This needs the Pro or Business plan. ## Put it on sale [#put-it-on-sale] The status button sits beside the product's name at the top of the page. 1. Choose the status button (it reads **Draft** on a new product). 2. Under **Who can see this product**, pick **Active**. If **Active** can't be picked, the panel lists what is still missing under **Before it can go live**, with a link to fix each one: - **Give it a price**: every type. - **Upload the file buyers get**: a file product. - **Publish at least one lesson**: a course. - **Put a product in it**: a bundle. - **Say what happens after they pay**: a service. Under the button, a short line tells you whether the product is **Ready to go live** or how many things are left to sort out. ## Archive, duplicate or delete [#archive-duplicate-or-delete] On **Products**, open the actions menu on a product's row. - **Archive** hides the product from your store. People who already bought it keep their access. **Unarchive** brings it back as a draft, so it goes through the same checks before it goes live again. - **Duplicate** makes a draft copy with its prices, editions, files and content, and opens it. Sales and stats stay with the original. Copied files count against your storage. - **Delete** removes the product with its prices, editions, files and content. Images stay in your media library. This can't be undone. A product that has sold can only be archived, never deleted. A product that sits inside a bundle can't be deleted until you take it out of the bundle. # Discount codes (/help/products/discount-codes) A discount code takes money off an order when a buyer types it at checkout. Codes belong to a store and work on any of its products. Find them under **Catalog**: open **Products**, then the **Discount codes** tab. ## Create a code [#create-a-code] 1. Choose **New discount code**. 2. Enter the **Code**, such as SUMMER20, or choose **Generate** for a random one. Use letters, numbers, hyphens and underscores. Buyers can type it in any case. 3. Pick the **Type**: - **Percentage**: a share off the order, in any currency. A percentage can't be more than 100. - **Fixed amount**: a set amount off, in one currency. 4. Enter the **Value**. 5. Set any limits or conditions you want (see below). 6. Choose **Create code**. ## Limits and conditions [#limits-and-conditions] All of these are optional. Leave a field empty and it doesn't apply. Under **Usage limits**: - **Total uses**: how many times the code can be used across all buyers. - **Uses per buyer**: how many times one buyer can use it. Under **Conditions**: - **Minimum purchase**: the smallest order the code works on. - **Maximum discount**: for a percentage code, the most it takes off one order, however large the order is. A fixed amount, a minimum purchase and a maximum discount all count in the code's **Currency**. A checkout in another currency can't use the code. ## The codes list [#the-codes-list] Each row shows the **Code**, its **Type** and **Value**, how many **Uses** it has had (out of its total, when there is a limit), and its **Status**. To stop a code working, choose the remove button on its row, then **Remove**. Buyers can't apply it after that. ## Codes for affiliates [#codes-for-affiliates] When your store has active affiliates, the **Credits** column lets you tie a code to one of them. A sale that uses the code is credited to that affiliate, whichever link the buyer arrived on. Pick **Nobody** to untie it. See [Affiliates](/help/affiliates). # Files and delivery (/help/products/files-and-delivery) What a buyer receives depends on the product's type. This page covers files, license keys, services and bundles. Courses have their own page: [Courses](/help/products/courses). ## Files [#files] A file product delivers every file you upload. Buyers get a download link on their receipt, and the files stay in their library to keep. 1. Open the product and find the **Files** card. 2. Choose **Upload file** and pick the file. 3. Wait for the upload to finish. The file's row shows its **Size** and **Status**. Upload as many files as you like; every buyer gets all of them. A product needs at least one finished file before it can go live. Each file must fit within your plan's per-file size limit, and a file over it shows **too large**. If an upload fails, the card says at which step. Choose **Retry** to try again, or **Discard** to drop it. Deleting a finished file takes it away from buyers who already own the product: it disappears from their library and their download links stop working. Uploading it again only reaches people who buy after that. A product sold in editions has a **Files** list inside each edition instead. See [Prices and editions](/help/products/prices). ## License keys [#license-keys] A license product gives every buyer their own key, shown on their receipt with your note on how to use it. Set two things in **Details**: - **Activations per key**: how many devices one key can activate. Empty means unlimited. - **Note to buyers**: shown beside their key. Say where to enter it and what it unlocks. The **License keys** card on the product lists every key issued, with its **Activations** and when it was **Issued**. Keys are hidden until you reveal them; you can also copy one. To stop a key working, choose the revoke button on its row, then **Revoke key**. Any app that checks the key refuses it from its next check. Revoking can't be undone. Seeing keys needs permission to view orders, and revoking needs permission to manage them. See [Members](/help/account/members). ## Services [#services] A service has nothing to download. After paying, the buyer sees your instructions on their receipt. Fill in at least one of these in **Details**: - **What happens next**: for example, how you will contact them and when. - **Booking link**: optional. It must start with `http://` or `https://`. With both empty the receipt says nothing, so the product can't go live. ## Bundles [#bundles] A bundle sells several of your products in one purchase, at the bundle's own price. Each product inside is delivered the way it normally is. 1. Open the bundle and find **What's included**. 2. Choose **Add a product**, pick one of your products, and choose **Add to bundle**. 3. Repeat for each product, then add a price. Draft products can't be added. For a license product inside a bundle, **Keys** sets how many license keys one purchase hands the buyer. The card compares prices for you: what the products cost bought separately, and what a buyer saves. When you add the bundle's price, the amount starts at what the products cost separately; type over it to set your own. To take a product out, use the remove button on its row, then **Take it out**. Buyers who already bought the bundle keep it; people who buy from now on won't get that product. A bundle on sale needs at least one product, so you can't take out the last one while it is active. # Products (/help/products) A product is the thing a buyer pays for. Each one has a type, at least one price, and whatever the buyer receives: a file, a course, a license key, your instructions, or other products in a bundle. Products live under **Catalog** in the sidebar. **Products** lists them, and the **Discount codes** tab beside it holds your codes. **Media** holds the images you use on products and pages. ## Product types [#product-types] You pick the type when you create the product, and it decides what the buyer gets. - **File**: something buyers download and keep, such as an ebook, a preset pack or a template. Buyers get every file you upload. - **Course**: lessons buyers work through inside Sorai. They sign in to watch and continue from where they stopped. - **License**: software or assets unlocked with a key. Each buyer gets their own license key on their receipt. - **Service**: work you deliver yourself after they pay. Buyers see your instructions on their receipt. - **Bundle**: several of your products sold together at one price. **Membership** shows as **Coming soon**. Sorai can't deliver memberships yet, so you can't create one. ## Who can see a product [#who-can-see-a-product] Every product has one of three statuses. - **Draft**: only you can see it. It isn't listed and can't be bought. New products start here. - **Active**: listed on your store, and buyers can pay for it. - **Archived**: hidden from your store. People who already bought it keep their access. A product can only go **Active** once it has what a buyer needs, such as a price and something to deliver. See [Create a product](/help/products/create-a-product) for the checklist. ## In this section [#in-this-section] Prefer the terminal? The [CLI](/cli) creates products, prices and media too. # Media (/help/products/media) Media is every image you have uploaded for this store. Upload an image once, then use it as a product's cover, in a product's gallery, or on a page. Open it from the sidebar: **Catalog**, then **Media**. ## Upload an image [#upload-an-image] 1. Choose **Upload image**. 2. Pick an image from your device. Images can be up to 20 MB. Each image shows its size and the date you uploaded it. Choose an image to see it as buyers will load it, with its **Dimensions** and when it was **Uploaded**. Use search to find an image by name or file type, and **Load more** to see older ones. If an upload fails, the screen says at which step. Choose **Retry** to try again, or **Discard** to drop it. ## Use an image on a product [#use-an-image-on-a-product] On a product, under **Selling content**, choose **Choose from library** next to the cover, or add an image to the **Gallery**. Pick an image you already uploaded, or choose **Upload image** to add a new one. See [Create a product](/help/products/create-a-product). Images you upload from a product's cover field land in Media too, so you can reuse them anywhere in the store. ## Delete an image [#delete-an-image] Choose the delete button on the image. Before you confirm, Sorai checks where the image is used: - **Nothing in this store uses this image.** It is safe to delete. - **Used by** the cover or gallery of a product, or a page, and those are live: buyers stop seeing the picture there right away. - **Used by** places that aren't on your live shop yet: nothing changes for buyers today, but those places lose the picture. Deleting a product doesn't delete its images; they stay in Media. # Checkout offers (/help/products/offers) Checkout offers let a buyer add another of your products while they are already buying. There are three, and each product can set its own: - **Order bump**: a checkbox on checkout that adds one more product to the order. For example: add the workbook for Rp 49.000. - **Offer after paying**: a one-tap offer right after payment, bought as its own order. For example: add the template pack for Rp 99.000. - **Cheaper offer if they say no**: shown only if the buyer declines the first offer after paying. For example: add the starter pack for Rp 49.000 instead. Checkout offers need the Pro or Business plan. ## Store offers and product offers [#store-offers-and-product-offers] Your store has its own order bump and post-payment offer, set from **Checkout** in the sidebar. Every product starts out showing those. See [Checkout](/help/selling/checkout). A product can follow the store, pick its own, or show nothing. Its own choice applies only to buyers of that product. ## Set a product's offers [#set-a-products-offers] 1. Open the product and find the **Checkout offers** card. 2. For each offer, pick one: - **Use what my store is set to** keeps the store's offer. - **Pick something for this product** lets you choose a **Product**. For the two offers after paying, choose its **Price** too. - **Show nothing here** turns the offer off for this product. 3. Choose **Save checkout offers**. Under each offer, **Today they see** tells you what buyers of this product are shown right now. The cheaper second offer follows the first one. If the product shows nothing after paying, there is no second offer either. If it uses the store's first offer, it also uses the store's second offer. ## Currency [#currency] An offer is priced in the currencies its product has prices in. Buyers paying in any other currency don't see it. The card tells you which currencies an offer covers. ## When an offer stops showing [#when-an-offer-stops-showing] The card warns you when an offer can't be shown, for example when the product you picked is no longer on sale or has been deleted. Until you fix it, buyers of this product see nothing in that place. Pick another product, or put the offered product back on sale. # Prices and editions (/help/products/prices) A product needs a price before it can go live. Each price is in one currency, and Sorai never converts between currencies: a buyer pays the amount you set in the currency they check out in. ## Add a price [#add-a-price] 1. Open the product and find the **Prices** card. 2. Choose **Add price**. 3. Pick the **Currency** and enter the **Amount**. 4. Choose **Add price**. A product holds one price per currency. The currency picker only offers the currencies your store can sell in. Adding a price in a second currency needs the Pro or Business plan. To change a price, choose **Edit** on its row, change it, and choose **Save**. To remove one, use the remove button on its row. You can't remove the only price on an active product: buyers would reach a checkout with nothing to charge. Add another price first, or set the product back to **Draft**. ## Let buyers choose the amount [#let-buyers-choose-the-amount] Turn on **Let buyers choose the amount** when you add the price. The amount you entered becomes a **Suggested amount**, and a buyer may pay more. Set a **Minimum** to stop buyers paying less than that, or leave it empty for no minimum. The minimum can't be more than the suggested amount. Pay what you want needs the Pro or Business plan. ## Sell in more than one edition [#sell-in-more-than-one-edition] Editions are versions of one product that a buyer picks between on the product page, such as Personal and Team. Each edition has its own price, files and settings. Files, licenses and services can have editions; courses and bundles can't. 1. On the product, choose **Sell this in more than one edition**. 2. Under **Call what you sell now**, name what you already sell. Its price and files stay exactly as they are and become the first edition. 3. Under **And add a second one called**, name the second edition. 4. Choose **Set up editions**. The **Prices** card becomes **Editions**, with one row per edition. ### Manage editions [#manage-editions] - **Add edition** adds another one. Give it a price before it goes on sale. - **Edition name** is what a buyer sees on the button they pick it with. - The **On sale** switch takes an edition on or off sale. The first edition is the product itself, so it goes on sale with the product. - The move buttons change the order buyers see them in. - A license edition has its own **Activations per key**. A service edition has its own **What happens next** and **Booking link**. A file edition has its own **Files**. **Remove edition** stops buyers being offered it and takes it off the list. Anyone who already bought it keeps everything they paid for. Editions go on sale when the product does. See [Create a product](/help/products/create-a-product) to put the product on sale. # Unpaid checkouts (/help/selling/abandoned-checkouts) A buyer who starts a checkout and doesn't pay leaves an unpaid order behind: it expires when the payment window closes, or the buyer cancels it. Sorai keeps these so you can see what was left unpaid and follow up. ## The recovery email [#the-recovery-email] Every buyer who leaves a checkout unpaid gets one email with a link back to their order. It tells them nothing was charged and lets them finish where they left off. You don't need to set anything up for this one. You can add a second email a day later, with a discount if you like: 1. Open **Checkout** in the sidebar, then the **Recovery** tab. 2. Turn on **Send a follow-up a day later**. 3. Optionally, pick a code under **Discount in the follow-up**. Only active discount codes are listed, and the code's own limits and expiry still apply. 4. Choose **Save**. The follow-up depends on your plan. Buyers can stop these reminders from a link in the email. To create a code, see [Discount codes](/help/products/discount-codes). ## See unpaid checkouts [#see-unpaid-checkouts] Open **Orders**, then the **Unpaid checkouts** tab. Each row shows the **Buyer**, **Product**, **Value**, when and how it **Ended** (**Expired** or **Cancelled by buyer**), and its **Recovery** state. Filter by **Period** (**Last 30 days**, **Last 90 days** or **All time**) and by recovery state. The summary at the top, where your plan includes it, shows per currency what came back, the **Recovery rate**, and what was left unpaid. Recovery states: - **No email yet**: the recovery email hasn't gone out. - **Reminder sent**: the first email has gone out. - **Follow-up sent**: the second email has gone out. - **Followed up**: you marked it as handled yourself. - **Unsubscribed**: the buyer asked to stop reminders. - **Recovered**: the buyer came back through the link and paid. - **Came back on their own**: the buyer bought the same product later, in a new order. ## Follow up yourself [#follow-up-yourself] Choose **Follow up** on a row to: - **Copy link**: copy a link that takes the buyer back to their order. - **Email the buyer**: open your email app with a short message and the link already written. - **WhatsApp the buyer**: the same, in WhatsApp. Shown only when the buyer left a phone number. - **Mark as followed up**: record that you have handled it, so your team knows. **Clear follow-up** undoes it. # Set up checkout (/help/selling/checkout) Every product in your store sells through one checkout. It always asks the buyer for their name and email. On the **Checkout** page you can add your own questions, offer extra products, and choose what happens once the payment succeeds. The settings apply to the whole store. Open **Checkout** in the sidebar, under **Sales**. The page has five tabs: - **Form fields**: the extra questions the checkout asks. - **Order bump** and **Post-payment offer**: see [Order bumps and offers](/help/products/offers). - **After payment**: where the buyer lands once they have paid. - **Recovery**: emails to buyers who leave without paying. See [Unpaid checkouts](/help/selling/abandoned-checkouts). Beside the form, **Preview** shows what the buyer sees at checkout and updates as you edit. It is not interactive. If your verification isn't finished, the page shows **Checkout opens after verification.** Buyers can't pay until it is done. See [Verification](/help/money/verification). ## Add a question [#add-a-question] 1. Open the **Form fields** tab and choose **Add field**. 2. Fill in **Label**: the question the buyer reads, such as "Company name". 3. Fill in **Key**: a short name the answer is stored under on each order, such as `company`. Every field needs one, and no two fields can share it. 4. Pick a **Type**: **Text**, **Email**, **Phone**, **Long text**, **Dropdown**, **Checkbox** or **Hidden**. 5. For a **Dropdown**, list the choices in **Options (one per line)**. 6. Turn on **Required** if the buyer must answer. 7. Choose **Save**. Use the arrows to change the order of the questions. The buyer's answers show on each order under **Buyer answers**. Custom fields depend on your plan. If your plan doesn't include them, the tab shows an upgrade notice. ### Renaming or removing a question [#renaming-or-removing-a-question] Orders you already took keep their answer under the key they were saved with. If you change a key, new orders store the answer under the new key, so anything that reads the answers sees both names. Removing a saved question asks you to confirm with **Remove field**. Past orders keep their answers; new orders no longer ask it. ### Hidden fields [#hidden-fields] A **Hidden** field never shows to the buyer. It picks up a value from the link that opened the checkout, which is useful for tags like a campaign or a source. Add `?key=value` to a checkout or product page link, using the field's key. Buyers can edit a link, so never use a hidden field to identify someone. If you build your own site, you can also pass hidden fields from code; see [Checkout in sorai-js](/sorai-js/checkout#hidden-fields). ## Choose where buyers land after paying [#choose-where-buyers-land-after-paying] Open the **After payment** tab and pick one: - **Stay on the page**: the buyer sees the confirmation and their purchase right where they paid. - **Redirect to a URL**: send the buyer to your own thank-you page. Enter a full `https://` address or a path on your storefront in **Redirect URL**. The order number is added to the address as `?order=`. This option depends on your plan. - **Open WhatsApp**: hand the buyer into a WhatsApp chat with you. Enter a phone number with its country code, or a full wa.me link. Choose **Save** when you are done. ## If saving doesn't work [#if-saving-doesnt-work] When something on a tab needs fixing, **Save** takes you to that tab and marks the problem. The usual ones: Fill in **Key** for each question. Give each question its own key. Add choices to the dropdown. Fix the **Redirect URL**. # Selling (/help/selling) Every product you sell goes through the same checkout. This section covers what that checkout asks, what it offers on the way, which payment methods buyers see, and how you hear about and measure each sale. Most of it lives in two places in the console: - **Checkout** in the sidebar, under **Sales**: the questions the checkout asks, the order bump, the offers after payment, where the buyer lands, and recovery emails. - **Store settings**, then **Payment methods** and **Integrations**: what buyers can pay with, tracking pixels, sale alerts and webhooks. Host your own site instead of a Sorai store page? The [sorai-js docs](/sorai-js) show how to open the same checkout from any page. # Sale alerts and webhooks (/help/selling/integrations) Sorai can tell you about each paid order as it happens, and send order events to tools you run yourself. Both live in **Store settings**, then **Integrations**: - **Notifications**: where sale alerts go. - **Webhooks**: order events sent to your own server. Notification channels and webhook endpoints belong to your organization and are shared by every store. Each store picks which channels hear about its sales. ## Sale alerts [#sale-alerts] Until you add a channel, sale alerts go by email to your organization's owners and admins. ### Add an email channel [#add-an-email-channel] 1. Open the **Notifications** tab and choose **Add channel**. 2. Under **Channel**, pick **Email** and enter the **Notify address**. 3. Choose **Add channel**. Sorai emails that address a link to confirm it. Nothing is sent there until the link is opened, and the channel shows **Waiting for confirmation** until then. If the link expired, use the resend button on the row to send a new one. ### Add a Telegram channel [#add-a-telegram-channel] 1. Choose **Add channel**, pick **Telegram**, then **Add channel**. 2. Sorai shows a one-time pairing link. Open it in Telegram and press Start. The channel turns active once paired. The link can't be shown again: if you lose it, remove the channel and add it back. A channel's destination can't be edited. To change it, remove the channel and add a new one. Use the switch on a row to turn a channel off or back on. ### Choose which channels hear about this store [#choose-which-channels-hear-about-this-store] Under **Where this store's sales go**, tick the channels for this store, or **Every channel, including ones added later**. Then choose **Save**. If nothing is ticked, no one is told about sales in this store. Orders still appear in **Orders**. ## Webhooks [#webhooks] A webhook sends order events to an address you own, signed with a secret only you hold. Use it to connect Sorai to your own systems. Webhooks depend on your plan. 1. Open the **Webhooks** tab and choose **Add endpoint**. 2. Enter the **URL** that should receive events. 3. Under **Events**, pick at least one, such as **Order paid** or **Order refunded**. 4. Choose **Add endpoint**. 5. Copy the **Signing secret** now. It is shown only once. From an endpoint's menu you can **Send a test**, **View deliveries** to see each attempt and what your server answered, or **Remove endpoint**. A failed delivery can be replayed from the deliveries list. For the payload, signatures and retries, see the developer guide to [webhooks](/api/webhooks). # Payment methods (/help/selling/payment-methods) The payment methods you turn on are the ones buyers see at checkout. Each method has its own fee and, for some, a minimum or maximum amount. You choose which methods to offer and whether you or the buyer pays the fee. Open **Store settings**, then **Payment methods**. If the list is empty, your account can't take payments yet. Methods appear once it can; see [Verification](/help/money/verification). ## Turn a method on or off [#turn-a-method-on-or-off] Methods are grouped by region (**Indonesia** and **Global**), then by kind: **QRIS**, **Virtual Account**, **E-wallets** and **Card**. 1. Find the method in its group. 2. Use its switch to turn it on or off. The change saves straight away and you see **Payment methods saved**. For virtual accounts, **Turn on all banks** and **Turn off all banks** change every bank in the group at once. The group heading shows how many are on. ## What each method shows [#what-each-method-shows] - **The fee** for accepting it, or **No fee**, with any minimum or maximum. - **Min** and **Max**: the smallest and largest amount a buyer can pay with it. - **Who pays the fee**: **Paid by you**, or **Paid by your buyer as a service fee**. - **Available in**: the currencies it accepts. A method marked **Not available in your currency** can't be offered for your prices. - **Refunds**: **Payments with this method can't be refunded.** or **Full refunds only**. This matters when you refund an order later; see [Refunds and cancellations](/help/orders/refunds-and-cancellations). Some methods carry a status: - **Disrupted**: the method is having problems. A note says what is going on. - **Paused**: buyers can't choose it right now. - **Unavailable**: it is no longer offered. If it is still on, turn it off. ## Choose who pays the payment fee [#choose-who-pays-the-payment-fee] The **Payment fee** card sets who pays the fee for accepting a payment. - **I pay the payment fee**: the price stays what you set. - **My buyers pay it as a service fee**: the fee is added to what they are charged at checkout. Choose one under **Who pays**, then **Save**. If the card says **Set by Sorai. Contact support to change it.**, the choice is fixed for your account. When buyers pay the service fee, a refund may or may not give it back. The refund dialog on each order tells you which. # Tracking pixels (/help/selling/tracking) Connect your ad and analytics accounts so they see the sales your campaigns bring in. Your store pages load the browser pixel for each ID you enter. For Meta, TikTok and Google Analytics 4, Sorai also reports each purchase from its own server, which still counts when a buyer's browser blocks ads. Pixels are set per store. Open **Store settings**, then **Integrations**, then the **Tracking** tab. Four cards show what is connected: - **Meta pixel**: purchase events for Meta ads. - **TikTok pixel**: purchase events for TikTok ads. - **Google Analytics 4**: purchase events for Google Analytics. - **Google Tag Manager**: your container, loaded on the storefront. Each card reads **Connected** or **Not set up**. Choose **Set up** to connect one, or **Manage** to change it. ## Connect Meta [#connect-meta] 1. On the **Meta pixel** card, choose **Set up**. 2. Enter your **Meta pixel ID**. In Meta Events Manager it is under Data Sources, then your pixel. 3. Enter a **Meta CAPI token**. In Events Manager, open Settings, then Conversions API, then Generate access token. This lets Sorai report purchases from the server. 4. Choose **Save**. To check the setup with Meta's Test Events tool, paste its code into **Meta test event code**. Clear it when you're done: live purchases that carry a test code don't count. ## Connect TikTok [#connect-tiktok] 1. On the **TikTok pixel** card, choose **Set up**. 2. Enter your **TikTok pixel ID**. In TikTok Events Manager it is under your pixel, then Details. 3. Enter a **TikTok CAPI token**. In Events Manager, open your pixel, then Settings, then Generate token. 4. Choose **Save**. **TikTok test event code** works like Meta's. TikTok issues its own code, separate from Meta's. ## Connect Google Analytics 4 [#connect-google-analytics-4] 1. On the **Google Analytics 4** card, choose **Set up**. 2. Enter your **GA4 measurement ID**. In Google Analytics it is under Admin, Data Streams, then your web stream. It starts with `G-`. 3. Enter a **GA4 API secret**. On the same web stream, open Measurement Protocol API secrets, then Create. 4. Choose **Save**. **Test** sends a test event. A pass means GA4 accepted the event format; it does not check the API secret. ## Connect Google Tag Manager [#connect-google-tag-manager] 1. On the **Google Tag Manager** card, choose **Set up**. 2. Enter your **GTM container ID**. In Tag Manager it is under Admin, then Container Settings. It starts with `GTM-`. 3. Choose **Save**. Tag Manager runs alongside the Meta, TikTok and GA4 pixels, not instead of them. Set those up on their own cards even if your container also has them. ## Tokens and secrets [#tokens-and-secrets] Tokens and API secrets are stored encrypted and never shown in full. When you manage a pixel later, leave the field untouched to keep the current value, or choose **Replace** to enter a new one. ## Check what was sent [#check-what-was-sent] Under the cards, the **Conversion API dispatch log** lists the server-side events sent to Meta, TikTok and GA4: the **Event**, the **Platform**, its status, and how many attempts it took. It shows the most recent attempts. If an event failed, use the resend button on its row to send it again. ## On your own site [#on-your-own-site] Selling from a site you host yourself? sorai-js can load your pixels there and send page and product events from the browser. See [Events and attribution](/sorai-js/events).