# Sharetribe Developer Documentation | Generated on: 2026-07-01T07:45:55.360Z ## Community contributions Description: Highlights contributions made by our developer community. Path: community-contributions/index.mdx ContributionCard, CustomCardColumn, } from '../../app/components/CustomCards/CustomCards'; # Community Contributions This page highlights a selection of community-built contributions created by developers within the Sharetribe community that extend what's possible on the platform by default. They showcase the creativity, skill, and initiative of our developer network, and provide resources that others can use, learn from, or build upon. Each project is independently developed and maintained by its author - Sharetribe does not own or maintain these contributions. Want to share ideas, suggest improvements, or build something of your own? [Join the conversation in our community Slack](https://www.sharetribe.com/dev-slack) --- ## Featured community projects {contributionsData.contributions.map((contribution, index) => ( ))} --- _This page is growing. If you've built something that extends the platform and want it featured here, let us know._ --- ## API Reference Path: concepts/api-sdk/api/index.mdx # API Reference
## APIs The Sharetribe API consists of the following individual APIs: - [Marketplace API](https://www.sharetribe.com/api-reference/marketplace.html) - [Integration API](https://www.sharetribe.com/api-reference/integration.html) - [Authentication API](https://www.sharetribe.com/api-reference/authentication.html) - [Asset Delivery API](https://www.sharetribe.com/api-reference/asset-delivery-api.html) All these are HTTP APIs and both the Integration API and Marketplace API support [JSON](https://www.ietf.org/rfc/rfc7159.txt) and [Transit](https://github.com/cognitect/transit-format) formats. The Authentication API is based on OAuth2. Our API documentation provides further technical detail on all four APIs. If you are looking for a way to use the APIs in your client or server application, refer to our documentation on the [SDK for JavaScript](https://sharetribe.github.io/flex-sdk-js/) or the [Integration SDK for JavaScript](https://sharetribe.github.io/flex-integration-sdk-js/). If you are interested in reading more about the Marketplace and Integration API, refer to our article on [Marketplace and Integration API](/concepts/api-sdk/marketplace-api-integration-api/). For more information on our Authentication API, refer to our article on the [Authentication API](/concepts/api-sdk/authentication-api/). For more information on the Asset Delivery API, refer to the [asset reference](/references/assets/) article. ## Postman collection You can also explore and test our API endpoints with our [Postman Collection](https://www.postman.com/sharetribe-api/workspace/sharetribe-public-workspace/collection/19129702-bd56ce4d-6877-4664-87a0-2cfdeed9db4d). In this collection you can find all available API endpoints. The collection uses pre-request scripts to authenticate requests. Bearer tokens are stored in collection variables. Therefore, you don't have to worry about authentication requests in this collection after you store client credentials in the collection variables. To start using the collection, create a Postman account and fork the collection. After that, add your API credentials to the Collection variables via the variables tab in Postman. Remember not to persist any credentials stored in variables if you're publicly sharing your forked collection. Click here to fork the Postman collection into your workspace: Run In Postman --- ## Authentication API Path: concepts/api-sdk/authentication-api/index.mdx # Authentication API ## Overview Both the [Marketplace API and the Integration API](/concepts/api-sdk/marketplace-api-integration-api/) require valid _access tokens_ to be passed in every API request. [Applications](/concepts/development/applications/) obtain those access tokens from the Authentication API. As a general rule, applications that access the Marketplace API do so by authenticating an end user of the marketplace (via the user' username and password), while Integration API applications authenticate using their own credentials. In order to access Sharetribe APIs, you need to create an _Application_ in [Console](https://console.sharetribe.com/advanced/applications). Each Application has a _client ID_. In addition, applications that access the Integration API also have a corresponding _client secret_. The easiest way to interact with both the Marketplace API and the Integration API is to use our [SDKs](/concepts/api-sdk/js-sdk/). The SDKs handle most of the complexity regarding authentication, access, and refresh tokens. Below, we discuss some of the underlying mechanisms and principles in the Authentication API. The Authentication API is based on the [OAuth 2.0](https://oauth.net/2/) framework. See also the [Authentication API reference documentation](https://www.sharetribe.com/api-reference/authentication.html) The _client secret_ is a secret value that must be kept safe and secure. Never expose your _client secret_ publicly (e.g. in your web site's HTML or JavaScript code, in your mobile app source code, etc). ## Grant types The Authentication API's main endpoint is for [issuing tokens](https://www.sharetribe.com/api-reference/authentication.html#issuing-tokens). Depending on whether your application is accessing the Marketplace API or the Integration API, that endpoint requires different set of parameters and issues different kinds of _access tokens_. Applications request access tokens using several different _grant types_: - `client_credentials` grant type is used by both Marketplace API and Integration API applications with some important differences: - when used by Marketplace API applications, it only requires the _client ID_ and grants _anonymous access tokens_ which can be used with any of the Marketplace API endpoints that provide public data about the marketplace (such as the [`/listings/`](https://www.sharetribe.com/api-reference/marketplace.html#listings) endpoints). - when used by Integration API applications, it requires both the client ID and the client secret and it grants access tokens that provide full access to the Integration API. It also provides a _refresh token_ that can be used to obtain fresh access tokens later - `password` grant type is used only by Marketplace API applications and allows to authenticate the marketplace's end users via their own _username_ and _password_. It also provides Marketplace API applications with a _refresh token_ that can be used to obtain fresh access tokens and can act as the end user's session secret. - `token_exchange` grant type is used by Marketplace API to create a trusted context for e.g. [privileged transitions](/concepts/transactions/privileged-transitions/) within transactions. It uses both _client ID_ and _client secret_, as well as a valid user access token obtained with `password` grant type. - `refresh_token` grant type is used by both Marketplace API and Integration API applications and grants a fresh _access token_ when given a client ID and a valid _refresh token_. All access tokens that the Authentication API grants are short lived (valid for some number of minutes). Instead of always using the main grant type repeatedly (i.e. `password` or `client_credentials`), implementations are advised to use the `refresh_token` grant, as refresh tokens are typically valid for much longer period of time (days to months). This practice minimizes the risk of a long term secret to be accidentally exposed (e.g. user's password or Integration API application's client secret). --- ## JavaScript SDKs Path: concepts/api-sdk/js-sdk/index.mdx # JavaScript SDKs The SDKs are the easiest way to interact with Sharetribe Marketplace API and the Sharetribe Integration API. They handle authentication, renewing authentication tokens and serializing and deserializing data to and from JavaScript data structures. Using the SDKs allows you to focus on building your marketplace front-end instead of implementing boilerplate to communicate with the API. ## Documentation Find our SDK documentation here: - [Sharetribe SDK for JavaScript Documentation](https://sharetribe.github.io/flex-sdk-js/) - [Sharetribe Integration SDK for JavaScript Documentation](https://sharetribe.github.io/flex-integration-sdk-js/) We also have a guide to get started with the SDKs: - [Getting started with the JavaScript SDKs](/introduction/getting-started-with-sdks/) ## Sharetribe SDK for JavaScript The Marketplace API is meant to handle all the interactions the end users of your marketplace take part in e.g. signing up, managing listings and transactions. You should be using the Marketplace API when building your marketplace UI, such as a web client or a mobile app. The Marketplace API only provides data that an individual user should have access to; this means that the API doesn't allow access to other users' private data, transactions or messages. The SDK provides a convenient way to access the Marketplace API endpoints without needing to configure custom API calls. You can access all the JS SDK documentation at: https://sharetribe.github.io/flex-sdk-js/ ### SDK Playground The SDK ships with a command-line based API Playground. You can use the Playground to try out the SDK with real API access to learn and test things. It also supports executing scripts against the API. To start the Playground, go to the directory where you cloned the SDK Git repository and type: ```bash $ yarn run playground --clientid ``` This will start the playground using your Marketplace API Client ID to connect to your marketplace. You can create a new Client ID or find your existing ones in Console at https://console.sharetribe.com/advanced/applications.
## Sharetribe Integration SDK for JavaScript In contrast to the Marketplace API, the Integration API reveals the entire marketplace data. The Integration API is meant for trusted secure applications to access. You might be running such an application in your own backend systems or as a 3rd party integration. The Integration API is well suited for integrations with your own or 3rd party systems, custom reporting and custom tooling for operators managing the marketplace. You can access all the JS SDK documentation at: https://sharetribe.github.io/flex-integration-sdk-js/ --- ## Marketplace API and Integration API Path: concepts/api-sdk/marketplace-api-integration-api/index.mdx # Sharetribe Marketplace API and Integration API ## Introduction Sharetribe provides two different main APIs for interacting with the marketplace and accessing its data: the _Marketplace API_ and the _Integration API_. In addition, Sharetribe also has the _Authentication API_, which is used to authenticate and obtain access credentials that are needed to access both the Marketplace API and the Integration API. For more information about the Asset Delivery API, see our **[reference documentation](/references/assets/)**. ## When to use the Marketplace API The Marketplace API enables all the core interactions that the end users of the marketplace participate in. These include signing up as a new user, managing listings, engaging in transactions, messaging, etc. The Marketplace API is best suited for building user-facing marketplace UI applications, like the marketplace's web site or mobile apps. With the Marketplace API, the authenticated party is always the end user and all API calls are made in the name of that authenticated user (with the exception of anonymous public access). Therefore, the Marketplace API allows access to the individual user's own data and any publicly available data. This allows the Marketplace API to be directly called from a user-controlled device or web browser, which is the case if you are using the open source [template application](/template/introduction/sharetribe-web-template/) provided by Sharetribe. The Marketplace API does not provide access to any data that an individual user should not access, such as other users' private data, transactions, or messages. The easiest way to access the Marketplace API is to use the [Sharetribe SDK for JavaScript](/concepts/api-sdk/js-sdk/#sharetribe-sdk-for-javascript). See also the [Marketplace API reference documentation](https://www.sharetribe.com/api-reference/marketplace.html). ## When to use the Integration API The Integration API allows trusted secure applications to access the entire marketplace data: all users, listings, transactions, messages, etc. Such trusted applications are for example applications that run in your own backend systems or applications meant to be executed by authorized marketplace operators. Never expose your Integration API [application](/concepts/development/applications/) credentials to an untrusted device or application, such as end user's browser or mobile app. The Integration API is well suited for building the following types of applications: - Integrations with other own or 3rd party systems, which can be stand-alone or work in combination with the end-user facing marketplace application and provide advanced features - Custom reporting - Custom tooling for operators managing the marketplace The easiest way to access the Integration API is to use the [Sharetribe Integration SDK for JavaScript](/concepts/api-sdk/js-sdk/#sharetribe-integration-sdk-for-javascript). See also the [Integration API reference documentation](https://www.sharetribe.com/api-reference/integration.html). ## Authentication Both the Marketplace API and the Integration API require valid _access tokens_ to be passed in every API request. Applications obtain those access tokens from the Authentication API. As a general rule, applications that access the Marketplace API do so by authenticating an end user of the marketplace (via the user' username and password), while Integration API applications authenticate using their own credentials. For more details see the [Authentication API](/concepts/api-sdk/authentication-api/) article and the [API reference documentation](https://www.sharetribe.com/api-reference/authentication.html). --- ## Rate limiting in Marketplace API and Integration API Path: concepts/api-sdk/rate-limiting/index.mdx # Rate limiting in Marketplace API and Integration API Starting in January 2023, Marketplace API and Integration API feature new rate limits in Dev and Test environments. In addition, Integration API features new concurrency limits in Dev and Test environments and, eventually, in Live environments.
What are rate limits and concurrency limits? - **Rate limit** means a limit on the number of requests that the API can process within a time span, e.g. in one minute. - **Concurrency limit** means a limit on the number of simultaneous requests that the API can process at any given moment.
The rate limits are different for queries (fetching data) and commands (modifying data). Queries are rate limited at 1 request per second (60 requests per minute) on average. Commands are rate limited at 1 request per 2 seconds (30 requests per minute) on average. The rate limit applies per client IP address. You can find more information on Sharetribe rate limits in the API reference for [Marketplace API](https://www.sharetribe.com/api-reference/marketplace.html#rate-limits) and [Integration API](https://www.sharetribe.com/api-reference/integration.html#rate-limits). It is good to note that **Live environments are currently not rate limited**, except for one endpoint in Integration API. Still, as you customize your Sharetribe marketplace implementation to take the rate and concurrency limits into account in your Development environment, we do appreciate it if you also transfer those behaviors into production. ## Interaction between different rate limits All query and command endpoints have rate limits in Dev and Test environments. In addition, the Integration API listing creation endpoint has a separate rate limit in all environments. This means that in Dev and Test environments, both rate limits apply to listing creation in the following way: - If the command rate limit burst capacity has not yet been depleted, listing creation is rate limited at 100 API calls per minute. This depletes the command rate limit burst capacity accordingly. - If the command rate limit burst capacity has been depleted, listing creation is rate limited at the regular command rate limit of 30 requests per minute. - If other commands are taking place while listings are being created, all those calls count towards the command rate limit. ## Handling rate limits in Marketplace API In standard usage, the rate limit will largely be undetectable in Marketplace API. The API calls happen, for the most part, as a consequence of user actions, so there is a natural delay between calls. This means that encountering rate limit errors while developing with Marketplace API can point you towards finding loops and other buggy behavior in your code. If your Test environment is exposed to the public web, a traffic spike caused by bots or crawlers can trigger rate-limiting, preventing server-side rendering from functioning. This may temporarily prevent access to your Test environment. Therefore, we strongly recommend using [basic HTTP authentication](/tutorial/deploy-to-render/#enable-http-basic-access-authentication) on all deployed test marketplace applications. ## Handling rate and concurrency limits in Integration API Rate limits will likely be more visible to you when developing with the Integration API. In addition, Integration API also features concurrency limiting, which needs to be handled. ### Integration SDK Sharetribe Integration SDK is configured to handle concurrency limits by default starting from version 1.9.0. To take this built-in concurrency limiting to use, you need to make sure your SDK is updated to at least this version. To handle the general rate limits in Dev and Test environments, you can pass configurations for query and command rate limiters. You can see the details of passing those configurations in our [SDK documentation](https://sharetribe.github.io/flex-integration-sdk-js/rate-limits.html). For listing creation rate limits, you will need to implement your own rate limit handling logic. We have an example of this in our [Integration API example scripts repository](https://github.com/sharetribe/integration-api-examples/blob/master/scripts/create-listings.js). It is good to note that rate limits apply by client IP address. If you have more than one instance of the SDK running on the same server or computer, then each SDK instance will rate-limit itself, but combined they might still go over the total limits. Read more in our [SDK documentation](https://sharetribe.github.io/flex-integration-sdk-js/rate-limits.html). If that is the case, you can: - customize the rate limits to lower values than the suggested ones, - customize `httpsAgent` to use `maxSockets` set to much lower than 10 (e.g. 3 to 5), so that the total number of concurrent requests has a lower chance of being over the limit - implement your own retry with random exponential backoff in case some requests still get a 429 response - or some combination of the above ### Integration API If you are not using the Integration SDK, we strongly recommend you to use your own rate and concurrency limit handling, by using e.g. a library such as [bottleneck](https://www.npmjs.com/package/bottleneck) for Node.js, or a similar existing implementation for your stack. --- ## Inventory management Path: concepts/availability/inventory-management/index.mdx # Inventory management In Sharetribe, you can manage the available stock or inventory of a listing via the stock-related features in the Marketplace API and the Integration API. With those APIs, you can determine the available stock (quantity) of any given listing as well as add to and subtract from it. Additions to stock will mostly be determined by the listing authors, as they restock the items they sell. Stock subtractions, on the other hand, will mostly happen as part of transactions, as buyers on your marketplace make purchases. This article describes the Sharetribe stock management features on a high level. We also have [a more technical article about stock management](/references/stock/). ## How do you determine the initial available stock or increase the available stock of a listing? With the stock-related APIs, you add to the available stock of a listing by creating a [stock adjustment](/references/stock/). This is an API call that you make through one of our APIs that lets your Sharetribe marketplace know that you have increased the quantity of available stock for one of your listings. This adjustment could be done directly through the marketplace UI or a third-party integration using the corresponding API calls. ## How do you remove or decrease the available stock of a listing? Most of the time, a listing's available stock will decrease because people purchase units via transactions. The stock-related transaction process actions allow defining your transaction process so that when a transaction is initiated, a stock reservation is made as well, for example. This prevents your users from purchasing more units than are available. If the transaction completes, the purchased units are removed from the inventory permanently. If the transaction is cancelled, the units are released back to the inventory and other users will be able to purchase them. Find out more about [transaction process actions related to stock reservations](/references/transaction-process-actions/#stock-reservations). You can also connect your Sharetribe marketplace with third-party systems to further manage stock. If units are bought through another site or system, you can sync this information with the Integration API and adjust your stock accordingly. Finally, the listing author could manually adjust their inventory directly from the marketplace interface, similar to how they would add inventory. ## Can listings be closed automatically if there is no stock left? Yes! This feature can be built into your Sharetribe marketplace app with relative ease, even though it is not part of the default template. Furthermore, even if you don't have a system in place that would close the listings automatically, users will not be able to purchase more units than are available, giving you peace of mind that no double-purchases of the same stock will happen. ## How to manage the number of seats or spaces for an event or a class? If you are looking to manage the number of spaces in an event, you should take a look at the seats feature. With seats, you can manage the number of available spaces in a given event at a given time. Seats are tied to [booking availability management](/concepts/availability/manage-seats/). --- ## Manage seats Path: concepts/availability/manage-seats/index.mdx # Manage seats In Sharetribe, you can manage the capacity of an event or space within a specific timeframe with seats. Seats is a fundamental feature for marketplaces that provide events, rentals, or services that can be booked by multiple people at the same time. Seats allow you to define the specific number of people that book the same time slot in an event, space, or service. If there are seats available in a specific time slot, the listing can still be booked by as many people as seats are available. Once all the seats are taken, the time slot becomes unavailable. This article describes seat management on a high level. If you want to learn how to manage seats from a technical perspective, visit the API references for [Marketplace API](https://www.sharetribe.com/api-reference/marketplace.html) or [Integration API](https://www.sharetribe.com/api-reference/integration.html). ## How do you define the available seats for a listing? You define the available seats for any given time slot of a listing via the [availability plan or availability exceptions](/references/availability/) of a listing. If the number of seats is set to 0, the listing will not be available at that time. In our Sharetribe Web Template, the default seat availability of any particular bookable listing is one. Listing authors set the availability plan and exceptions of their listing during listing creation. Users determine when their listing is available and when it’s not within the timeframes your marketplace offers: hourly, daily, or custom length. When modifying the template, you or your developers can enable more than one seat per time slot, either in general or for specific dates and times. If your marketplace listings happen at a specific time or place instead of at a repeated interval (e.g. concerts or events), you can set the listing availability to be blocked by default. Listing authors can then create availability exceptions to open availability for the day(s) of their event and the desired number of seats. To set availability as blocked by default, you need to set an availability plan with 0 seats across the board. Some examples: - You can set multiple available spaces for the same sauna for every night. - You can set available beds in a hostel room. - You can set the maximum number of people that can participate in a yoga class depending on the day. - You can determine the number of screens that can participate in an online cooking class. ## How do you decrease the available seats of a listing? Most of the time, a listing's available seats will decrease through bookings via transactions. The [booking-related transaction process actions](/references/transaction-process-actions/#bookings) allow defining your transaction process so that when a transaction is initiated, a seat reservation is made as well. This prevents your users from booking more seats than are available. If the transaction completes, the seats are removed from the listing's or timeslot's availability. If the transaction is cancelled, the seats are released and other users will be able to book them. Find out more about [availability related transaction actions](/references/transaction-process-actions/#bookings). You can also connect your Sharetribe marketplace with third-party systems to further manage seats. If bookings are made through another system, you can sync this information using the Integration API and adjust listing availability plans accordingly or override the plan with an exception. Finally, listing authors could manually reduce the number of seats or block their availability entirely for a specific time slot (date or hour) directly from the marketplace interface, similar to how they would determine their initial availability. ## Can listings be searched by available spots? Yes! Listing search can be modified so that available seats are taken into account. It's possible to search for listings that have desired number of seats available on specific dates or times. For example: - Find listings that have 2 seats available on next Friday. - Find listings that have 5 seats available for two hours some time next week. If a marketplace uses availability-based listing search, then listings that don't have enough spots available will be automatically filtered out, even though they are available for a lower number of people in the same timeframe. ## Can I manage stock or inventory of a listing with seats? If you are looking to manage the stock or inventory of a listing, you should take a look at [stock management in Sharetribe](/concepts/availability/inventory-management/). --- ## Content management in Sharetribe Path: concepts/content-management/content-management-in-sharetribe/index.mdx # Content management in Sharetribe A Sharetribe marketplace allows you to manage content in Console without making code changes. ## Sharetribe Content management glossary Before diving deeper into content management in Sharetribe, here is a short glossary of the most central content related terms. ### Content management The way in which operator-created content is handled within Sharetribe. Before Console-operated content management, operators could either - add the content directly in the client app codebase, - use the [marketplace text](/concepts/content-management/marketplace-texts/) files for marketplace content, or - integrate an external content management system. Currently, we recommend managing content using the assets-based Pages feature in Sharetribe Console. ### Content modeling The process of structuring patterns of content in a way that allows operators to create content using existing building blocks, and that allows designers and developers to create ways of showing the content in a systematic way. ### Page The collection of different content elements under a specific URL. Pages can have fields, sections, or blocks within them. A page can be fetched as [an asset](/references/assets/) from the Sharetribe Asset Delivery API. Example: `landing-page`. ### Page asset As the operator adds and edits the elements of the page in Sharetribe Console, a page asset is created under the hood. The page asset uses a **page schema** to describe the different content elements in a way that the client application can then understand and render. ### Section The main element of a page. In Sharetribe, each section has a specific content model that determines the information that the section must have to function properly. Sections can have fields and blocks within them. In Sharetribe, sections are the main way to distinguish pages from each other. Example: article, feature, columns. ### Block Blocks are optional, but they are very useful for more extensive pieces of content. Different section content models can show blocks in different ways, depending on the design. In addition, adding images to sections happens within blocks. Blocks can have fields within them. Example: default block. ### Field The simplest content element in Sharetribe. Conveys a single piece of information. Example: text field, button, image. ## Content models When managing content, it is useful to distinguish content modeling from content. A **content model** means the different elements that can comprise e.g. a single page – headings, text blocks, buttons, images and so forth. A single content model that features a heading, a text block, an image and a button, can be used to create several different pages with different **content**. The benefit of content modeling is that when you have a set of distinct content models, the content creator can choose the one that best suits their needs without needing to worry how the content will look on the page. The designer and developer, on the other hand, can design e.g. article pages or feature pages without needing to have the specific pieces of content already written. Instead, the content model provides the designer with the necessary information to create a layout that looks great and covers each part of the model. A content model also facilitates using the same content in different channels – your mobile application can have one design and your website can have another, but they can both use the same content models. In Sharetribe, the content modeling step is currently done for you. Each **page** can have a selection of pre-determined **sections**, which in turn can have several **blocks**. Depending on which kinds of sections and blocks you choose for your page, you can create a wide range of content. In the future, it will also be possible to create custom sections. ![Landing page with levels of content modeling elements](./content-model-landing-page.png) ## Sections Sharetribe has four default content sections that you can select for your page. All sections have a title, an ingress, and a button for a call to action. ### Article Article sections are great for prose text. If your content is longer, you can add blocks with different levels of headings and text paragraphs. Blocks are displayed in a single column one after the other, as if on a blog page. ![Article section](./pages-article-section.png) ### Columns Columns sections divide blocks into pre-defined columns, and the operator can decide the number of columns between 1-4. The columns pattern is great for featuring specific options in parallel, whether they are benefits, testimonials, or featured locations. ![Columns section](./pages-columns-section.png) ### Features Features sections, by default, show blocks of visual and text content. The block contents are on a single row, alternating the order, and different blocks are in a single column. As with all content sections, the designer of your website can make changes as to how exactly the blocks get displayed on the page. ![Features section](./pages-features-section.png) ### Carousel The carousel section shows content blocks side by side, in a carousel that can be browsed by swiping or scrolling. Carousel sections work great for a long array of blocks that the reader may or may not want to investigate in more depth. ![Carousel section](./pages-carousel-section.png) ## Blocks Currently, Sharetribe has a single type of block that you can add to any section. The block contains a title, and you can determine the level of the title (page title, section title, section subtitle). A block can also have text content and a button. Block text content can be modified with Markdown. This allows the content creator to use even more fine-grained editing, including italics and bolding, subheadings, links, and code snippets. ## Content delivery Once the page has been created, it is fetched as an [asset](/references/assets/) to the client application using the [Asset Delivery API](https://www.sharetribe.com/api-reference/asset-delivery-api.html). For the landing page, the asset being modified in Sharetribe is `content/landing-page.js`. Assets can be fetched by the latest version, or a specific version. The client application then shows the page content it has fetched. The Sharetribe Web Template is configured to show asset-based content by default. For legacy template versions that do not have asset-based content capabilities, you can refer to our [legacy template documentation](/template/legacy/legacy-templates/). To see your page changes in your client application, you need to save your changes in Sharetribe Console. Then, you can click the "View page" link next to the page title. ![View page](./view-page.png) This will open the page in your **Marketplace URL** address. ### Publishing pages from test to live When you first take your marketplace live, your Live marketplace environment is created as a duplicate of your Test (or Dev) marketplace. This includes all your page assets. You can also modify your marketplace content pages after going live. You will still need to make and test the changes in your Test environment. Then, after you are happy with all your page changes, you can click the "Copy to..." button. After clicking the button, you will see a modal detailing which pages have been modified, created, and removed compared to your current live pages. You can check the boxes of the pages you want to copy to Live. This will override the current live content of those pages with the new content from the Test environment. Once you have copied a page to Live, you cannot return to the previous live version of that page. Take extra good care to double check your changes in Test environment before copying anything to Live! It can take up to five minutes for your changes to update from Test to Live environment. You can also copy your asset changes to the Dev environment. ## Content layout Content modeling does not, by default, contain information how the content should be laid out in the client application. The Sharetribe web template does have components corresponding to the different content sections. However, the Sharetribe content assets can be used in any client application, and even in the template you have full freedom as to how the different sections get displayed. This means that even with the pre-defined options, you can create a page setup that is entirely your own. Read more about how the template shows content created in Sharetribe Console: - [How the Sharetribe Web Template renders content pages using the PageBuilder](/template/content-management/page-builder/) --- ## Using Console and the API to manage Pages Path: concepts/content-management/headless-content-management/index.mdx # Using Console and the API to manage Pages Pages is a lightweight headless content management system that allows you to build content pages. Pages can be created and edited through the Sharetribe Console, and the data can be queried via API, allowing you to edit content pages through a visual interface. ## How is Pages "headless"? The Pages feature allows content editors to make changes to content pages without having to touch code. Pages is a "headless" feature, where the content is decoupled from the frontend, and can be accessed via API. In Sharetribe, the presentational layer, or frontend, is usually the Sharetribe Web Template (if you are not using a custom client application), and the content is the data that is managed through Pages. The headless architecture allows you to render the content in any client you choose, as it can be retrieved by a call to the Sharetribe API. To illustrate further, here is an example of how data fetched from the API maps to visual elements in the default "About page" in the template: ![How data is rendered on the about page](./data.png) ## Querying the Asset Delivery API The content you edit through Console can be queried through the Asset Delivery API. The template queries this data automatically and renders the content on dynamic content pages using it. Learn more about how the template renders content pages [here](/template/content-management/page-builder/). A basic query to the Asset Delivery API to fetch the content of the landing page looks like this: ```bash curl https://cdn.st-api.com/v1/assets/pub/client_id/content/pages/landing-page.json ``` Remember to replace `client_id` with your client ID. Through the Sharetribe SDK, this query is formatted as: ```js sdk.assetByAlias({ path: 'content/pages/landing-page.json', alias: 'latest', }); ``` You can try this out in the [SDK playground](https://sharetribe.github.io/flex-sdk-js/try-it-in-the-playground.html), curl, or by pasting the Asset Delivery API query into your web browser's address bar. If you fetch the data through your browser, you should see something like this: ![Example data](./example-data.png) This is the raw JSON API response that encodes the content and structure you have entered into Console. Try pasting the response into your preferred code editor to make it easier to interpret: ![Example data formatted](./formatted.png) This data is meant for your client application to interpret. Sharetribe Web Template can automatically render a landing page using this data. Read more about the structure of page asset data: - [Page asset schema reference](/references/page-asset-schema/) To see how changes in the Console are reflected in the actual data, let's make a change to the landing page through Console. Edit the title value through the Page Editor: ![Changing a value through Console](./console-change.png) Fetch the most recent data from the Asset Delivery API either through the SDK or through your browser. You'll see that the change we made in Console is now visible in the JSON data: ```json { "data" : {"sections": [ { "title": { "content": "Test title" //... ``` --- ## Marketplace texts in Sharetribe Path: concepts/content-management/marketplace-texts/index.mdx # Marketplace texts in Sharetribe Marketplace texts cover all the small pieces of text in your marketplace's dynamic pages – button labels, error messages, and help texts, for example. Modifying marketplace texts to match your marketplace's theme and tone of voice is a key task in customizing any marketplace. [Read more about marketplace texts](https://www.sharetribe.com/help/en/collections/8975298-editing-text-content) ## Marketplace texts in the Sharetribe Web Template In the Sharetribe Web Template, marketplace texts are not written directly into the source code. Instead, the source code uses [ICU message formatting](https://unicode-org.github.io/icu/userguide/format_parse/messages/) that defines keys for each meaningful piece of content, and a translator or a content creator can then define the message (i.e. the value) for each key in their language. The end user only sees the content creator's words, not the key itself, unless the key is missing a value in the selected language. The key - value syntax is as follows: ```json ".": "" ``` For example: ```json "ManageListingCard.editListing": "Edit listing" ``` The key is then used in the code, so that the code does not need to be changed even if the value ends up changing. ```js // ManageListingCard.js uses the variable to identify the message ``` In addition to adding a marketplace text file in the client application, Sharetribe marketplace operators can modify the wording of marketplace texts in Sharetribe Console > Build > Content. This means that operators can make changes to the marketplace texts without the need for code changes. The same marketplace texts can also be used from several different client applications, making it easier to make centralized changes. ![Simple Console marketplace texts](./microcopy_simple.png) ## Email texts in Sharetribe Marketplace email templates also use a similar formatting with a [custom Handlebar helper](/references/email-templates/#handlebars). The key-value syntax is similar to the one Sharetribe Web Template uses: ```json ".": "", ``` For example, ```json "EmailChanged.Title": "Your email address was changed", ``` The email template syntax uses the key with the Handlebar helper `t`. In addition to the message key, the helper expects a fallback message, in case the message key does not exist. ```handlebars

{{t 'EmailChanged.Title' 'Your email address was changed'}}

``` Similarly to marketplace texts, email texts can also be modified in Console > Build > Content. ## How marketplace texts are handled in Sharetribe Console-editable marketplace and email texts in Sharetribe are based on a concept of [assets](/references/assets/). Assets provide a way to define marketplace content and configurations using JSON files without needing to include the actual content in the client application codebase. ![Content view for editing marketplace texts](./marketplace-text-editor.png) Marketplace texts are fetched to the client application through Asset Delivery API in JSON format. Assets can be retrieved by the latest version, or by a specific version. Read more: - [Marketplace assets](/references/assets/) - [Handling hosted asset marketplace texts in the Sharetribe Web Template](/template/content-management/hosted-marketplace-texts/) Since email notifications are sent directly from Sharetribe, the email text asset is used automatically as soon as it exists. ## Format for editing marketplace and email texts in Console A piece of marketplace text using [ICU message formatting](https://unicode-org.github.io/icu/userguide/format_parse/messages/) can, at its simplest, consist of a phrase. ```json { "ManageListingCard.editListing": "Edit listing" } ``` In the template, the phrase is then passed to the UI element that shows the value. ```js // ManageListingCard.js uses the variable to identify the message ``` ![Simple marketplace text phrase in UI](./microcopy_UI_simple.png) In the email templates, the phrase is passed to the Handlebar helper `t`, which either renders the message itself, or the fallback message. Read more about [using marketplace texts in the Sharetribe Web Template](/template/content-management/how-to-change-bundled-marketplace-texts/#using-marketplace-texts). ### Simple argument In addition, the format supports passing parameters as arguments to the marketplace text string. Passing a [simple argument](https://formatjs.io/docs/core-concepts/icu-syntax/#simple-argument) allows showing context-specific information as a part of the marketplace text string. ```json { "ManageListingCard.pendingApproval": "{listingTitle} is pending admin approval. It's not visible on the marketplace yet." } ``` Using the message then requires that the code passes parameter `listingTitle` to the element that renders the value. The `{ listingTitle }` parameter will then be replaced with whatever the listing's title is. ```jsx ``` ![Marketplace text phrase with parameter in UI](./microcopy_UI_parameter.png) For the email texts, a simple argument works in a similar fashion. ```json { "NewMessage.MessageSentParagraph": "{senderName} sent you a message in {marketplaceName}." } ``` In the email template, all parameters that are used either in the ICU message or the fallback message need to be defined within the `t` helper. ```handlebars

{{t "NewMessage.MessageSentParagraph" "{senderName} sent you a message in {marketplaceName}." senderName=sender.display-name marketplaceName=marketplace.name}}

``` ![Email text phrase with parameter in Console](./email_text_parameter.png) Do note that even if the message uses a simple argument, you can choose to not use it. For instance, you could replace the message in the previous example with `"ManageListingCard.pendingApproval": "This listing is pending admin approval and can't be booked."`. However, if you later decide you do want to use the title, it is recommended to double check the original marketplace text file in your client application to see the names of the attributes available in the message. ### Pluralization One important factor in creating natural marketplace texts is handling pluralization in a text. The ICU format makes it possible to define different wordings for singular and plural options. ```json { "ManageListingsPage.youHaveListings": "You have {count} {count, plural, one {listing} other {listings}}" } ``` When you use plural in the marketplace text string, you will need to specify - the variable determining which option to use (here: `count`) - the pattern we are following (here: `plural`) - the options matching each alternative you want to specify (here: `one` – there could be several options specified) - an `other` option that gets used when none of the specified alternatives matches ```js ``` ![Message with pluralization in UI](./microcopy_UI_plural.png) You can use pluralization, too, in email templates. ```json { "BookingNewRequest.PriceForHoursQuantity": "{amount, number, ::.00} {currency} × {units, number} {units, plural, one {hour} other {hours}}" } ``` Here, too, we define - the variable determining which option to use (here: `units`) - the pattern we are following (here: `plural`) - the options matching each alternative you want to specify (here: `one` – there could be several options specified) - an `other` option that gets used when none of the specified alternatives matches ```handlebars

{{t 'BookingNewRequest.PriceForHoursQuantity' '{amount, number, ::.00} {currency} × {units, number} {units, plural, one {hour} other {hours}}' amount=unit-price.amount currency=unit-price.currency units=units }}

``` ![Message with pluralization in email](./email_text_plural.png) Since different languages have different pluralization rules, pluralization is defined per language. You can see the full list of pluralization arguments (`zero`, `one`, `two`, `few` etc.) in the [ICU syntax documentation](https://formatjs.io/docs/core-concepts/icu-syntax/#plural-format). ### Selection In addition to pluralization options, you can build logic to the marketplace text strings using [select formatting](https://formatjs.io/docs/core-concepts/icu-syntax/#select-format). When you use `select` in the marketplace text string, you will need to specify - the variable determining which option to use (here: `actor`) - the pattern we are following (here: `select`) - the options matching each alternative you want to specify (here: `you` – there could be several options specified) - an `other` option that gets used when none of the specified alternatives matches ```json { "TransactionPage.ActivityFeed.default-purchase.purchased": "{actor, select, you {You placed an order for {listingTitle}.} other {{otherUsersName} placed an order for {listingTitle}.}}" } ``` You can then use the message in the code e.g. with the `formatMessage` function: ```js const message = intl.formatMessage( { id: `TransactionPage.ActivityFeed.${processName}.${nextState}` }, { actor, otherUsersName, listingTitle, reviewLink, deliveryMethod, stateStatus, } ); ``` ![Message with select logic](./microcopy_UI_select.png) ```json { "PurchaseOrderMarkedAsDelivered.Subject": "Your order for {listingTitle} was {option, select, shipping {shipped} other {delivered}}" } ``` ```handlebars {{set-translations (asset 'content/email-texts.json')}} {{t 'PurchaseOrderMarkedAsDelivered.Subject' 'Your order for {listingTitle} was {option, select, shipping {shipped} other {delivered}}' listingTitle=transaction.listing.title option=transaction.protected-data.deliveryMethod }} ``` ![Email subject with select logic](./email_text_select.png) You can use `select` for cases where you have a predetermined list of options you will encounter that require different marketplace text strings. ## Can I have a multilanguage marketplace? Having several language-specific marketplace text files enables using a single application for multiple languages. However, editing marketplace texts in Console only supports one language at a time, so you will need to modify any other languages using bundled marketplace text files within your client application. Having multiple languages in a single marketplace may, however, cause a problem in terms of user-generated content. Even though listings and user profiles could include both language versions by saving the content of language-specific input fields to a listing's extended data, users are rarely capable of providing content for several languages. With email notifications and built-in emails, at the moment only a single file of translations is supported. However, it is possible to dynamically construct the translation keys used in the `t` helper, by using the `concat` helper to concatenate the key with data that is available in the template context. This mechanism can be used to support multiple languages in the new email templates, like in the following example: In _content/email-texts.json_: ```json { // Translation key for the email subject in English "NewMessage.Subject_en": "{senderName} has sent you a new message" } ``` In the template: ```handlebars {{! Dynamically construct the translation key based on the recipient's language }} {{t (concat 'NewMessage.Subject' '_' recipient.private-data.language) '{senderName} has sent you a new message' senderName=message.sender.display-name }} ``` Read more about the `concat` helper in the [custom handlebar helper reference](/references/email-templates). In the example, the language is stored in the recipient's private data. Read more about what to consider when [building a multilanguage Sharetribe marketplace on top of Sharetribe Web Template](/template/content-management/how-to-change-template-language/#developing-the-sharetribe-web-template-into-a-multilanguage-marketplace). --- ## Applications Path: concepts/development/applications/index.mdx # Applications Applications are used to manage the credentials you use to access the Sharetribe APIs. ## Add a new application In Sharetribe, accessing the [Marketplace API or the Integration API](/concepts/api-sdk/marketplace-api-integration-api/) starts with creating an [_application_](https://console.sharetribe.com/advanced/applications). Creating an application will provide you with the Client ID and a Client Secret. These credentials are used to [authenticate](/concepts/api-sdk/authentication-api/) via the Authentication API. Using an Access Token returned by the Authentication API, you can make calls to the Marketplace API and the Integration API. ![Screenshot of the "Add a new application" modal](./add-a-new-application.png) Applications come in two types: Marketplace API applications and Integration API applications. Both types of applications have both a client ID and a client secret. As a general rule, the client ID can be considered public information, while the client secret must always be kept secure. ## Best practices - Create a separate application for each of your systems that access the Sharetribe APIs. For example, if you have a web and a mobile app, create two separate Marketplace API applications. - Name your applications so that it is easier for you to recognize how each one is used. For instance, use names like "Web UI", "iOS mobile app", "reporting", "mailing list integration", etc. - Store your application credentials in a secure place, such as a password manager. The client ID of each application is visible in Console, but you can only view the client secret when the application is first created. This means that if you haven't stored your client secret, you will not be able to view it later. - When an application is deleted, all API clients using the associated Client ID and Secret will stop working. Always keep your applications' client secret secure. Never expose it in a untrusted device or application, such as end users' browsers or mobile apps. --- ## The edn format Path: concepts/development/edn/index.mdx # The edn format Sharetribe transaction processes and migration content uses Clojure **edn** format. This article describes the basics of reading and writing edn for the purposes of working with the Sharetribe Developer Platform. ## Sharetribe's transaction process Sharetribe's transaction process description uses a format called **edn**. At first glance it may seem a bit odd if you have not seen edn before. However, there are many similarities with the JSON format. Here's a small example of edn: ```clojure ;; This is a comment. Comments in edn start with ";;" ;; {:number 1 ;; a number, for example `1`, `2.2`, `-500`, `1.23456M` ;; (where `M` denotes that exact precision is desired) :string "This is a string" :boolean true ;; or false :keyword :this-is-a-keyword :namespaced-keyword :namespaced/keyword :vector [1, "abc", false] ;; same as "array" in JSON :map {:first-name "John", :last-name "Doe", :age 55} ;; same as "object" in JSON } ``` Keywords are used heavily in the process description syntax as keys in maps as well as enum values. Keywords start with a `:` but are otherwise similar to strings. Keywords can have a namespace, in which case they are called qualified keywords, or be plain (unqualified). The part before `/` is the namespace. So for example, `:actor.role/customer` is a keyword in the namespace `actor.role`. Commas (`,`) in edn are optional and often omitted, but can be used for clarity. For a longer example, this is an edn representation of the transition **expire-review-period**. It illustrates some of the structures mentioned in the earlier example. ```clojure ;; The transition itself is defined as a map with the following plain keywords as keys: ;; - name ;; - at ;; - actions ;; - from ;; - to ;; The values have different formats ;; - the value of :name is a qualified keyword in the namespace 'transition' {:name :transition/expire-review-period, ;; - the value of :at is a map that represents a time expression function :at ;; - the function name here is :fn/plus, a qualified keyword in the namespace 'fn' ;; - the function parameters are a vector {:fn/plus ;; this function has two parameters in the vector: ;; - a map of a timepoint function for booking end time {:namespace/keyword, [:namespace/keyword]} ;; - the timespan to be added with :fn/plus to the timepoint {:namespace/keyword, ["string"]} [{:fn/timepoint [:time/booking-end]} {:fn/period ["P7D"]}]}, ;; the value of :actions is an empty array :actions [], ;; the value of :from is a qualified keyword in namespace 'state' :from :state/delivered, ;; the value of :to is a qualified keyword in namespace 'state' :to :state/reviewed} ``` You can use the Sharetribe CLI to validate your transaction process. Make sure you are using the latest CLI version when validating the process to avoid unnecessary errors. {/* prettier-ignore */} ```shell yarn global add flex-cli ``` ```shell npm install --global flex-cli ``` {/* // prettier-ignore */} You can see the CLI version you are using by running `flex-cli version`. Note that when you push and pull your process with the Sharetribe CLI, inline comments in the process.edn file are not maintained. ## Creating your edn file for a data migration When you are creating new documents with edn, such as migration imports, we recommend that you use an edn library to generate the data and validate the .edn syntax to ensure that the file is up to standard. edn libraries exist for multiple languages, for example [JavaSript](https://www.npmjs.com/search?q=edn), [Python](https://pypi.org/search/?q=edn), and [Ruby](https://rubygems.org/search?query=edn). Clojure has built-in support for edn. Usually these libraries support encoding data structures to edn and creating custom tagged elements. Here's an example of how to write Intermediary data – a Sharetribe proprietary data migration format – in edn format using JavaScript libraries [jsedn](https://www.npmjs.com/package/jsedn) and [uuid](https://www.npmjs.com/package/uuid) to represent a user and a listing: ```javascript const edn = require('jsedn'); const { v4: uuidv4 } = require('uuid'); const tagged = (tag, value) => new edn.Tagged(new edn.Tag(tag), value); const uuid = () => tagged('uuid', uuidv4()); const price = (amount, currency) => tagged('im/money', [amount, currency]); const email = (data) => { const { emailAddress } = data; return new edn.Map([ edn.kw(':im.email/address'), emailAddress, edn.kw(':im.email/verified'), true, ]); }; const profile = (data) => { const { firstName, lastName } = data; return new edn.Map([ edn.kw(':im.userProfile/firstName'), firstName, edn.kw(':im.userProfile/lastName'), lastName, ]); }; const user = (data) => { const { alias, emailAddress, firstName, lastName } = data; const role = new edn.Vector([ edn.kw(':user.role/customer'), edn.kw(':user.role/provider'), ]); return new edn.Vector([ new edn.Vector([edn.kw(':im.user/id'), uuid(), alias]), new edn.Map([ edn.kw(':im.user/primaryEmail'), email(data), edn.kw(':im.user/createdAt'), tagged('inst', new Date().toISOString()), edn.kw(':im.user/role'), role, edn.kw(':im.user/profile'), profile(data), ]), ]); }; const listing = (data) => { const { alias, title, priceAmount, author } = data; return new edn.Vector([ new edn.Vector([edn.kw(':im.listing/id'), uuid(), alias]), new edn.Map([ edn.kw(':im.listing/title'), title, edn.kw(':im.listing/createdAt'), tagged('inst', new Date().toISOString()), edn.kw(':im.listing/state'), edn.kw(':listing.state/published'), edn.kw(':im.listing/price'), price(priceAmount, 'EUR'), edn.kw(':im.listing/author'), tagged('im/ref', author), ]), ]); }; const userAlias = edn.kw(':user/john'); const e = new edn.Map([ edn.kw(':ident'), edn.kw(':mymarketplace'), edn.kw(':data'), new edn.Vector([ listing({ alias: edn.kw(':listing/rock-sauna'), title: 'A solid rock sauna', priceAmount: 12.2, author: userAlias, }), user({ alias: userAlias, emailAddress: 'foo@sharetribe.com', firstName: 'John', lastName: 'Doe', }), ]), ]); console.log(edn.encode(e)); // or save to file ``` Read more about the Intermediary format and migrating existing data to Sharetribe in this article: - [Migrating from outside Sharetribe](/how-to/migrations/migrating-from-outside-sharetribe/) --- ## Sharetribe environments Path: concepts/development/sharetribe-environments/index.mdx # Sharetribe environments Sharetribe environments are instances of your own marketplace, but with different functions. When you first create a Sharetribe account, you have one environment: Test. If you start customizing your marketplace with code, you can enable a second environment – Dev. Finally, when you are ready to go live, we will create a third environment: Live. The environment can be changed from the dropdown in the top of the sidebar. ![Environment selection](./env-change.png) **Organization** and **environment** are terms that are used to communicate which Sharetribe marketplace you are looking at in Console. **Organization**: An entity that is created when you first create an account. This is where you can invite other admin users to work with. An organization can include multiple environments for different purposes, but it can only include one Live environment. **Environment**: A marketplace instance within your organization which can be created for different purposes. There are three different types of environments: Dev, Test, and Live. You can access them all with the same Sharetribe account. ## Environment types The three environments in Sharetribe each have their own specific purpose. Each environment should also have its own dedicated client application, which also follows the purpose of the environment. ### Test environment The Test environment works as a preview environment for Live. Whereas a possible Dev environment is meant for the developer to make code changes, Test is meant to reflect your Live environment as accurately as possible. The operator can make no-code changes in Test, and copy them to Dev and Live without needing a developer to intervene. Because Test and Live are identical, the operator can trust that their changes made in Test show up correctly when published to Live. Copying no-code assets to Dev makes it easy for developers to be working on the correct configurations in Dev. Do not onboard real users or listings to Test, as they cannot be moved into Live. ### Dev environment The dev environment is for development purposes. This is where building your marketplace happens and where you can explore the build functionalities in peace by using test users and [test credit cards with Stripe](/introduction/set-up-and-use-stripe/). Whenever the development team wants to publish their code changes, they will fully test and review them in Dev first, and then copy them to Test and Live at the same time. If development work requires new configurations to be copied from Dev to Test (such as listing fields), they need to be recreated manually in Test, and can then be copied to Live. Even after launching your marketplace, you can continue building new features in Dev without causing disruptions to your Test or Live marketplaces. Note that you should not onboard real users or listings to Dev, as they cannot be moved into Live. ### Live environment The Live environment is where the business happens: here you can onboard your real customers and listings, and your customers can make real money transactions. When the necessary development has been done and your marketplace is ready for onboarding real users, you can initiate the Live environment setup from Console. This is also the point when you start paying the Sharetribe subscriptions (see more information about [Sharetribe pricing](https://www.sharetribe.com/pricing/)). ## Workflow between the three environments In a nutshell, the workflow between the environments is that changes flow from Dev to Test to Live: - code changes are made and reviewed in Dev, and get pushed from Dev to Test and Live - no-code changes are made in Test, and get pushed from Test to Dev and Live. ![Sharetribe environments workflow](./flex-environments.png) Code changes include - client application development, updated through your code repository - transaction process changes, updated through Sharetribe CLI - search schema changes, updated through Sharetribe CLI No-code changes include - Configuration changes in Console, like adding new listing fields - Content changes in Console, like editing your landing page We recommend that you keep Test and Live identical as much as possible. In other words, push any code changes from Dev to Test and Live at the same time before making further no-code changes in Test. This will ensure that Test accurately works as a preview environment for Live. If you have made asset changes in Live or Dev, and would want to move them to Test to start managing them according to this workflow, you can contact the Sharetribe support team through the chat widget in your Console. We can help you move the assets as a one-time operation. If you prefer a workflow where you can copy assets directly from Dev to Live without going through Test, contact the Sharetribe support team through the chat widget in your Console. We can enable this workflow for your organization on request. ## Additional development environments Depending on your development flow, you might need additional dev environments for your organization, e.g. for Quality Assurance (QA) or automated testing. We can include additional environments to your paid subscription at a price of $49 per month per environment. To include additional development environments in your subscription, contact Sharetribe Support! --- ## Sitemap support in Sharetribe Path: concepts/development/sitemap-in-sharetribe/index.mdx # Sitemap support in Sharetribe Having a sitemap on your marketplace helps search engines to crawl and index your website correctly. For a traditional website, sitemaps are relatively easy to make, because all of your routes are built in to the application. However, on a marketplace, it is likely that you will have listing URLs that include dynamic data from each listing – and you still want search engines to index those pages. Sharetribe offers API endpoints for fetching sitemap data for [listings](https://www.sharetribe.com/api-reference/marketplace.html#query-list-of-listings) and [CMS pages](https://www.sharetribe.com/api-reference/marketplace.html#query-list-of-assets). The Sharetribe Web Template has tooling to create a dynamic sitemap by default. Read more about the template sitemap structure and logic: **[Sitemap in Sharetribe Web Template](/template/analytics/sitemap-in-template/)** If you are creating your own custom application, you can consider whether you want to use the same practices that the web template uses to craft the sitemap. ## Separate sitemaps for static and dynamic content Static content sitemaps help search engines quickly index unchanging pages like your "About" page, while dynamic content sitemaps focus on frequently updated or user-generated content, such as listings in your marketplace. For example, the Sharetribe Web Template generates three different sub-sitemaps linked from _sitemap-index.xml_, all with slightly different data: - _sitemap-default.xml_ exposes public built-in pages in the template. Non-public routes that require authentication are disallowed in the _robots.txt_ file. - _sitemap-recent-listings.xml_ exposes routes to the most recently created listings. The listing data is fetched from a specific [_sitemapData_ endpoint](https://www.sharetribe.com/api-reference/marketplace.html#query-list-of-listings). The endpoint returns the 10 000 most recent listings. - _sitemap-recent-pages.xml_ exposes routes to the CMS pages created in Console that do not have built-in paths in the client application. The page data is fetched from a specific [_sitemapData_ endpoint](https://www.sharetribe.com/api-reference/marketplace.html#query-list-of-assets). It is good to note that the sitemap API endpoints cache results for one day, so sitemaps generated using the sitemap API endpoints do not update in real time. If your marketplace has more than 10 000 listings, you will need to fetch the information for older listing through Integration API. Since the Integration API [listing query endpoint](https://www.sharetribe.com/api-reference/integration.html#query-listings) is paginated, you will need to filter the results by e.g. the _createdAt_ attribute to retrieve all listings. ## Caching In addition to the API endpoint caching, the template has its own cache for one day, so the combined cache effect can be up to two days. You can consider what kind of caching approach would work in your client application. Especially on bigger sites with multiple listings, listing sitemap data can take up to a few seconds to fetch from the SDK. Caching the results improves the sitemap performance. ## Resources Read more about sitemaps from Google: - [Building a XML sitemap](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap#xml) - [Managing large sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps) - [Introduction to robots.txt](https://developers.google.com/search/docs/crawling-indexing/robots/intro) --- ## Introduction to extended data Path: concepts/extended-data/extended-data-introduction/index.mdx # Introduction to extended data This article explains the basics of extended data. If you want to get technical instead, check out the [Extended data API Reference](/references/extended-data/). ## Why extended data? Extended data is a Sharetribe feature that allows you to customize your user, listing, and transaction data. Your marketplace has its own unique offering and requires specific data that other marketplaces do not. Maybe you're building a marketplace for cooking classes and want to ask chefs how many years of experience they have. Or perhaps you're building a summer cottage rental community and want your providers to define the amenities of their cottage. Extended data gives you the freedom to determine exactly what information you want your users to provide on your marketplace and how. However, the possibilities of extended data do not end there! Extended data can be customized to different use-cases to fit your exact needs: in addition to collecting the information you need from your users in the form you choose, it allows you to display featured listings, have different user types, build custom search functionality, and much more. With extended data, you can build integrations with third-party services, such as a subscription payment system or SMS notification software. You can also have extended data that is only revealed at a specific point in a transaction. Or maybe you want more control over how search results on your marketplace are prioritized and sorted? For all these customizations, extended data is your friend. The possibilities you have with extended data are vast. In the next section, we'll discuss each type of extended data in more detail and offer examples of what they can be used for. ## Types of extended data There are six possible types of extended data, defined by who can edit and view them. Five out of these are available in Sharetribe at this time. They are _public data_, _protected data_, and _private data_, as well as _public metadata_ and _protected metadata_. In the following sections, "author" means the user who created the listing or profile in question. "Operator" refers to both the marketplace owner and the Integration API. The marketplace operators and the Integration API have access to view and edit all of the data types. ### Access to edit Extended _data_ can be written and edited by listing or user profile authors in your frontend application. _Metadata_ can be written and edited only by marketplace operators. ### Access to view Public data and public metadata can be viewed by everyone with access to your marketplace. Protected data is private by default, but can be viewed at a certain point during a transaction process by members of that transaction. Protected metadata is visible to the participants of the transaction. Private data can only be viewed by the listing or user profile authors themselves. We can also organize the data types by placing them in a table. | | Data | Metadata | | :-------- | :------------------------------------------------------- | :----------------------------------------------- | | Public | editing: author, operator – viewing: all users | editing: operator – viewing: all users | | Protected | editing: author, operator – viewing: transaction members | editing: operator – viewing: transaction members | | Private | editing: author, operator – viewing: author, operator | not available | In order to determine what type of extended data you want to collect on your marketplace, you need to answer the following questions: - What information do you want to collect about your users and listings and during transactions? - Who can write and edit that information? - What information do you want to display and to whom? In the next section, we'll explore how different types of extended data are shown on your marketplace and Console and offer examples of the possibilities the different types of extended data provide. ## Using extended data ### Public data Public data is information that is visible to all users of your marketplace and can be written and edited by listing authors or user profile owners. It can help your customers make purchasing decisions, let your customers know important details about your sellers, or be used as search filters and parameters and to sort search results. Public data allows you to customize your public listing and user information to fit your needs exactly! Let's look at listing public data in action. Here is a listing from an imaginary bike rental marketplace, Biketribe. We have highlighted the fields in different colors: blue is for tire size, and green is for bike brand. ![An image highlighting the extended data attributes in the client application](./extended-data-frontend-view.png) You can define when in the listing creation process each extended data attribute is collected. In this marketplace, the public data attributes shown on the listing page are a part of the listing details. You can also add separate tabs to add listing public data. ![An image of the client app editing the details of a listing](./extended-data-details.png) This is the same listing in Console, your marketplace management tool. You can see the corresponding public data fields highlighted with the same color as on the listing creation page. ![An image from Console showing the extended data attributes](./extended-data-console-view.png) Further public data you might want to collect could be website links or relevant social media handles in user profiles. Public data can be any type of information you believe will be important for your buyers to have or your sellers to share to get the most out of your marketplace. ### Protected data Protected data is information that can be revealed at specific points of the transaction process. It can only be seen by the parties taking part in the transaction, meaning the provider, customer, and the marketplace operator. After a cooking class booking is confirmed, you might want to request the customer to provide information on any dietary restrictions. Or maybe you only want to reveal a provider's phone number or address after payment has been confirmed to guarantee your users do not bypass your payment system. These cases can be handled with protected data. Other examples of protected data could be a link to the provider's Zoom page or a link to download a digital file the buyer has purchased. Or maybe your marketplace is for car rentals, and you want the customer to provide photos of the rented vehicle before and after the rental period. All these and more can all be included as protected data. ### Private data Private data can only be edited and viewed by those who created the listing or user profile in question and marketplace operators. It is similar to protected data but is not intended to be revealed during the transaction process. Private data can be used to collect and store information about users or listings that is important for marketplace operators but does not need to or should not be revealed to other users. Private data is especially useful in third-party integrations. You can store an ID from an external service to user or listing private data and connect it to services such as SMS notifications with Twilio or sync the provider's schedule with Google Calendar! As a further example – even though you may not want your customers and providers to be able to contact each other outside of your platform, you might still want to be able to call them yourself. A user's phone number can be saved in their private data for these situations. Private data can also be used if you want the listing author to give specific information for your listing approval process. Maybe you run a marketplace for graphic designers and want to verify their experience with past employers or check their portfolio before publishing a listing. Contact details of previous employers and links to online portfolios could be included as private data. ### Public metadata Public metadata is visible to all users, but only the operator and the Integration API can edit it. Typical use-cases for metadata are featured listings or premium users. You may want to curate listings that get this extra visibility yourself or offer it as a paid service, so using public data, which the users can edit themselves, is not an option. This is where metadata comes into play. Like public data, public metadata can be used as search filters and parameters and in sorting search results. In addition to featured listings, other ways to use public metadata could be to distinguish verified users from regular ones or highlight Gold members who are part of your highest subscription tier. Maybe you want to waive the marketplace commission for them. Based on the user's subscription information saved in their metadata, you can trigger a transaction process with or without a commission fee. Or maybe you want to establish one-time payments for users to get to promote their listings on your landing page: data of such payments can be saved in public metadata. You can use this metadata to always display featured listings first in relevant search results, for example. ### Protected metadata Transactions can also have metadata. It can only be seen by the transaction members as it is tied to the transaction. An example of transaction metadata could be a unique Zoom link to where an online service will take place. This metadata can be written into the transaction by the Integration API at a specific point of the transaction, or it can be added in Console by the operator. You can also configure the transaction process to update transaction metadata. ## Getting started with extended data Extended data is a powerful feature that allows you to customize your marketplace's offering, whether services, rentals, or products, to your exact needs. It helps you collect the information you require from your users and enables additional functionality together with the transaction process and through the Integration API. Extended data also plays a vital role in search result sorting and filtering. To get started with extended data, you should decide what information you want to collect about your users and listings or what is important for users to know during transactions. Next, you should think about who has access to edit that information. Finally, you should consider whether you want to display this information to everyone, select users, or just to yourself. Through extended data, Sharetribe can support a multitude of different kinds of listings, monetization models, user profiles, and so on. You can create the exact data structure you need for your marketplace. --- ## Listing extended data Path: concepts/extended-data/listing-extended-data/index.mdx # Listing extended data Listings can have three types of extended data: public, private, and metadata. This article gives an overview of using these different extended data types. Listing public data fields can be configured in Console using [assets](/references/assets/). The Sharetribe Web Template has the capability to read the asset-based public data fields and display the necessary components when editing and viewing a listing. ## Viewing and modifying listing extended data Public data and metadata are visible to everyone – in other words, they are available when querying listings through the [public listing endpoints](https://www.sharetribe.com/api-reference/marketplace.html#listings) in Marketplace API. Public data and metadata can be used, for instance, to distinguish different types of listings from each other, or to allow marketplace users to filter and search for specific features on a listing. Operators can use metadata to categorise listings to regular and premium, for instance. On the other hand, listing private data is available through the [ownListing endpoints](https://www.sharetribe.com/api-reference/marketplace.html#own-listings) in Marketplace API and [listing endpoints](https://www.sharetribe.com/api-reference/integration.html#listings) in Integration API. Private data can be used to allow the listing author to make private notes on the listing, since the information is not visible for the general marketplace audience. The listing's author can modify the listing's public and private data through the [ownListing](https://www.sharetribe.com/api-reference/marketplace.html#own-listings) create and update endpoints. An operator can modify all listing extended data, either through [Integration API](https://www.sharetribe.com/api-reference/integration.html#listings) or in Sharetribe Console. ## Search and filtering How users search and filter listings is a vital part of their experience in your marketplace. A smooth search experience allows them to find the listings they're interested in effortlessly, and the right filters help them narrow down results to a selection that best fits their needs. Extended data helps you build the custom search and filtering experience your marketplace needs. Listings can be searched by keyword or location using Sharetribe's [powerful built-in search feature](/concepts/listings/how-the-listing-search-works/). In addition to this, you can use listing public extended data and metadata to create a variety of different types of filters; for example, a filter can be a slider with a range of values or a checkbox group. You can also specify how listings are prioritized and sorted in the results. Extended data is not available for search or sorting by default, which means you are in control of building your own, unique search experience. When planning your search experience, think about the following questions: Do you want the extended data in any given field to be searchable? Do you want it to be a filter as well as a search parameter? What kind of filter should it be? Which extended data should be prioritized in search results? Maybe your marketplace charges a membership fee, and you want listings from sellers in your highest subscription tier to be displayed first. Or perhaps your marketplace is for selling preowned clothing, and you want your users to be able to filter by size to find the best fit. The possibilities are numerous! ## Different types of listings A marketplace can have several types of listings – rentals and sales, events and facilities. Listing extended data is a powerful way to distinguish different listing types. Read more about the possibilities for [different listing types in Sharetribe](/concepts/listings/listings-overview/#different-types-of-listings). --- ## Transaction extended data Path: concepts/extended-data/transaction-extended-data/index.mdx # Transaction extended data Transactions have two types of extended data. Both transaction protected data and metadata are visible for the transaction participants and operators. Protected data can only be updated using transaction process actions. Transaction metadata, on the other hand, can be updated through [a privileged transaction process action](/references/transaction-process-actions/#actionprivileged-update-metadata). In addition, operators can update metadata either through Sharetribe Console or using an [Integration API endpoint for updating transaction metadata](https://www.sharetribe.com/api-reference/integration.html#update-transaction-metadata). Transaction protected data fields can be configured in Console using [assets](/references/assets/). Starting from release [v10.12.0](https://github.com/sharetribe/web-template/releases/tag/v10.12.0), the Sharetribe Web Template has the capability to read the asset-based protected data configurations in listing type assets, and display the necessary components when editing and viewing a transaction's details. ## Protected data is updated through the transaction process Transaction protected data is visible to both transaction parties. It can be updated by [revealing the participants' protected data](/concepts/extended-data/user-extended-data/#revealing-information-within-the-transaction) to the transaction, which renders it visible within the transaction. In addition, a transaction process can have an action to [update transaction protected data](/references/transaction-process-actions/#actionupdate-protected-data). Protected data can be used to store any transaction specific information that the users are allowed to modify. Most common use cases include customer or provider contact information, but e.g. for more complex marketplace setups with several related transactions, any related transaction ids can be stored in protected data. ## Metadata can be updated from trusted contexts Transaction metadata is visible to both transaction parties, but it can only be updated by the operator. If your marketplace has integrations to third party services whose information is transaction specific, like discount services, you can store information such as integration references or discount codes or percentages in transaction metadata. Metadata can also be used to tag transactions for analytics purposes. You need a trusted context for updating transaction metadata – either a privileged transition, or a secure server endpoint. You can also build a separate application to [listen to transaction events](/how-to/events/reacting-to-events/) and update transaction metadata through the Integration API as a reaction to those events. ## Transaction filtering in Marketplace API and Integration API Both Marketplace API and Integration API have endpoints for querying transactions: - [Marketplace API /transactions/query](https://www.sharetribe.com/api-reference/marketplace.html#query-transactions) - [Integration API /transactions/query](https://www.sharetribe.com/api-reference/integration.html#query-transactions) To facilitate transaction querying, it is possible to filter transactions also by extended data when calling these endpoints. That way, you can segment your transactions by important attributes for more fine-grained processing. To filter transactions by extended data, you will need to [create a search schema](/how-to/search/manage-search-schemas-with-sharetribe-cli/) for the transaction. --- ## User extended data Path: concepts/extended-data/user-extended-data/index.mdx # User extended data User profiles have all four extended data types – public data, protected data, private data, and public metadata. A user's extended data is nested in the user's _profile_ attribute, not a direct attribute of the user resource. User public and private data fields and user types can be configured in Console using [assets](/references/assets/). Starting from release [v5.0.0](https://github.com/sharetribe/web-template/releases/tag/v5.0.0) for user fields and release [v5.2.0](https://github.com/sharetribe/web-template/releases/tag/v5.2.0) for user types, the Sharetribe Web Template has the capability to read the asset-based extended data configurations and display the necessary components when editing and viewing a user's profile. Private user fields can be configured in Console starting from template release [10.3.0](https://github.com/sharetribe/web-template/releases/tag/v10.3.0) ## Viewing and modifying user extended data User public and metadata are visible through the [users/show](https://www.sharetribe.com/api-reference/marketplace.html#show-user) endpoint in Marketplace API. In addition, the authenticated user can view their own extended data through the [current_user/show](https://www.sharetribe.com/api-reference/marketplace.html#show-current-user) endpoint. Operators can see all user extended data through the [user retrieval endpoints](https://www.sharetribe.com/api-reference/integration.html#users) in Integration API. Authenticated users are able to modify their own public, protected and private extended data through the [current user creation and update endpoints](https://www.sharetribe.com/api-reference/marketplace.html#current-user) in Marketplace API. Operators can modify all user extended data using the [users/update_profile](https://www.sharetribe.com/api-reference/integration.html#update-user-profile) in Integration API. ## Types of users By default, Sharetribe marketplace users have the capabilities to be both providers and customers. However, you may want to limit those capabilities, for instance by only allowing verified marketplace users to create listings. Or you may want to have a three-way marketplace where only one type of participant can be in several roles – yoga teachers may want to hire spaces from venues as customers and then offer their events as providers. You can achieve these types of distinctions by setting specific attributes in your users' extended data and allowing them access to certain parts of the marketplace based on those attributes. Starting from release [v8.5.0](https://github.com/sharetribe/web-template/releases/tag/v8.5.0), operators can differentiate users' experience based on user type in Console, and the Sharetribe Web Template will display selected user interface elements differently based on those configurations. You can also implement different tiers of listing authors, where for example a regular (non-paid) user is allowed to publish one listing and a premium (paid) user is allowed to publish three. These types of attributes will need to be set to the user's metadata, so that only operators can modify their listing author tier. ## Revealing information within the transaction User extended data can also be used for sensitive information that the transaction participants may want to only reveal to the other participant of the transaction. This type of information can include contact information such as phone numbers, or e.g. instructions to access a listing's goods or services in some other way. As a part of your marketplace's transaction process, then, you can have an action that reveals the user's protected data in the transaction. In other words, the revealed protected data becomes also available in the transaction's protected data, and can be shown to either one of the transaction participants. ## User filtering in Integration API The Marketplace API does not have an endpoint for querying users. However, when you are building an integration or creating analytics, you can use the [user query endpoint in Integration API](https://www.sharetribe.com/api-reference/integration.html#query-users). To facilitate user querying, it is possible to filter users also by extended data when calling the endpoint. That way, you can segment your users by important attributes for more fine-grained processing. To filter users by extended data, you will need to [create a search schema](/how-to/search/manage-search-schemas-with-sharetribe-cli/) for the user profile. --- ## File lifecycle in Sharetribe Path: concepts/files/file-lifecycle/index.mdx # File lifecycle in Sharetribe Sharetribe has endpoints and mechanisms for uploading and downloading digital files. Uploading and downloading files are both multi-step operations, and there are a number of different entities and states involved. ## Uploading a file ### Read file metadata Before you make any API calls, you need to get the metadata of the file in the local file system. The SDK has a helper function for parsing the attributes of the file metadata that are needed for uploading the file. ```js const { file: sdkFile } = sharetribeSdk; const metadata = sdkFile.metadata(file); ``` If you are using the APIs directly, you'll need to parse the necessary metadata information from the `File` object: ```js const metadata = { name: 'File to be uploaded.pdf', mimeType: 'application/pdf', size: 10697, }; ``` ### Create an ownFile resource With the metadata information, you can create an `ownFile` resource in the Sharetribe backend with `ownFiles.create`. ```js const ownFileResource = await sdk.ownFiles.create({ ...metadata }).data; ``` This step also validates that the file mime type is supported. Blocked mime types include potentially harmful types such as shell scripts, executables, and disk images. ### Create a file upload URL The id of the `ownFile` resource is then used to create a signed file upload URL. ```js const fileId = ownFileResource.data.id; const fileUploadDetails = await sdk.fileUploads.create({ fileId, }); ``` ```json { "fileUploadDetails": { "status": 200, "statusText": "", "data": { "data": { "id": { "uuid": "63abdec4-85e7-4157-8e99-2e2c7465d8be" }, "type": "fileUpload", "attributes": { "fileId": { "uuid": "69fd8859-8380-49c0-ab7e-0ee502e40811" }, "url": "https://unique-signed-sharetribe-upload-url.com", "headers": { "Content-Type": "application/pdf" }, "method": "PUT", "expiresAt": "2026-05-08T08:16:30.988Z" } } } } } ``` ### Upload the file to Sharetribe storage with the file upload URL The actual file is uploaded directly to Sharetribe storage with the URL and other details from the response, and the upload does not use Sharetribe APIs. The SDK has another helper function to upload the file. When using the SDK, you can also pass in an upload progress tracking callback to display the progress to the user. ```js const { method = 'PUT', url, headers = {}, } = fileUploadDetails?.data?.data?.attributes; const onUploadProgress = (progressEvent) => { const loaded = progressEvent?.loaded || 0; const total = progressEvent?.total || file.size; const progress = total ? Math.min(100, Math.round((loaded / total) * 100)) : null; console.log(`progress ${progress} %`); }; sdkFile.upload({ method, url, headers, file, onUploadProgress, }); ``` ### Attach the file to a resource The file id of the `ownFile` resource is then used to attach the file to another resource, e.g. to a message: ```js sdk.messages.send({ transactionId: '6985dfd3-34bc-4bc8-806f-03a0f29e056d', content: 'This is a message with a file', publicFileAttachments: [fileId], }); ``` The file resource (`file` or `ownFile`) can be in one of several states: - pendingUpload - pendingVerification - available - verificationFailed Depending on your use case, you may want to allow attaching files that have completed upload but still verifying, or only allow attaching files that are available. After the file is attached to a resource, it is visible according to the scope of the `fileAttachment`. ## Downloading a file To download a file, the user makes a POST request for a short-lived download URL for the file from Sharetribe API, and uses that generated URL to fetch the file from Sharetribe storage. Download links are only created for files in the `available` state. There are two different download endpoints that use different ids: - sdk.ownFileDownloads.create for ownFile resources, where the parameter is the `file` id - sdk.fileDownloads.create for file resources, where the parameter is the `fileAttachment` id ```js const downloadDetails = isOwnFile ? sdk.ownFileDownloads.create({ fileId }).data?.data : sdk.fileDownloads.create({ fileAttachmentId }).data?.data; const { url } = downloadDetails?.attributes; ``` ## Deleting a file An operator can delete a file in Console. When attaching files to a message, a single file is only attached to a single message at a time, so when an operator deletes a file associated with a message, both the `fileAttachment` and the `file` are deleted. There is no file deletion endpoint in either Integration API or Marketplace API. If you've uploaded a file but you have not yet attached it to a message, leaving the file unattached will result in it being eventually deleted. --- ## Files in Sharetribe Path: concepts/files/files-in-sharetribe/index.mdx # Files in Sharetribe Sharetribe supports uploading and downloading files and attaching them to different marketplace resources. The file information and relationships to other resources are stored in the Sharetribe backend and available through the Sharetribe APIs. The file entities themselves are stored in a third-party storage separate from the Sharetribe APIs, and you need to call the Sharetribe APIs to request unique signed upload and download URLs to access the actual file entities. - Call Sharetribe APIs to - create a file resource - retrieve file information - create upload and download URLs for a file resource - manage resources with file attachments - Call Sharetribe storage to - upload file using a short-lived upload URL - download file using a short-lived download URL ![Files architecture in Sharetribe](./files-architecture.png) ## Permissions Uploading and downloading files is enabled by default. Operators can disable uploading and downloading files across the marketplace in Access control. When uploads and downloads are disabled in Console > General > Access control, [file upload and download endpoints](/concepts/users-and-authentication/user-access-control-in-sharetribe/#disabling-file-uploads-and-downloads) will throw a 403 Forbidden error. In addition, you can use listing types or user types to enable or disable specific use cases for files in your marketplace, if files are enabled. For example, to view and use file attachment capabilities in transaction messages when using the default Sharetribe Web Template, you'll need to double check that files are enabled both through access control and in the listing type of the transaction's listing. ## file and fileAttachment In the Sharetribe system, `file` and `fileAttachment` represent two different things: - A `file` represents a single uploaded digital file resource - A `fileAttachment` is a file's connection to another resource, such as a message. A `file` is a related resource to a `fileAttachment`, and a `fileAttachment` is a related resource of another resource, for example a message. In this structure, it is possible for a `file` to have `fileAttachment` links to multiple different resources. When querying the resource, you'll need to include both the file attachments relationship and the nested file relationship if you want to retrieve both. ```js const messages = await sdk.messages.query({ transaction_id: txId, include: ['publicFileAttachments', 'publicFileAttachments.file'], }); ``` ```js // messages.query API response with included // publicFileAttachments and publicFileAttachments.file { "data": [ { "id": { "uuid": "69fd886a-96b2-4ba1-b458-cda5fdc0616e" }, "type": "message", "attributes": { "content": "message content", "createdAt": "2026-05-08T06:53:30.043Z", "deleted": false }, // publicFileAttachments is a relationship to message "relationships": { "publicFileAttachments": { "data": [ { "id": { "uuid": "69fd886a-1924-41c5-bfe1-a43deb7e1c3b" }, "type": "fileAttachment" } ] } } } ], "included": [ { // this is the file resource that has a relationship to fileAttachment "id": { "uuid": "69fd8859-8380-49c0-ab7e-0ee502e40811" }, "type": "file", "attributes": { "name": "File to be uploaded.pdf", "state": "available", "size": 10697, "deleted": false } }, // This message has one fileAttachment in its publicFileAttachments array { "id": { "uuid": "69fd886a-1924-41c5-bfe1-a43deb7e1c3b" }, "type": "fileAttachment", "attributes": { "scope": "public", "deleted": false }, // file is a relationship to fileAttachment "relationships": { "file": { "data": { "id": { "uuid": "69fd8859-8380-49c0-ab7e-0ee502e40811" }, "type": "file" } } } } ], "meta": { "totalItems": 1, "totalPages": 1, "page": 1, "perPage": 100 } } ``` An operator can delete a file in Console. When attaching files to messages, a single file is only attached to a single message at a time, so when an operator deletes a file associated with a message, both the `fileAttachment` and the `file` are deleted. ## file and ownFile The digital file resource can be represented by two resources: `file` and `ownFile`. To fetch a public `file` resource, you'll need to use the id of the associated `fileAttachment`, as the `fileAttachment` resource contains the scope information that informs whether the API caller has access to the resource. ```js const file = await sdk.files.show({ fileAttachmentId }).data.data; ``` ```json "file": { "id": { "uuid": "69fd8859-8380-49c0-ab7e-0ee502e40811" }, "type": "file", "attributes": { "name": "File to be uploaded.pdf", "state": "available", "size": 10697, "deleted": false } } ``` There's also an `ownFile` resource, which is the author's version of the file. In addition to the public attributes, the `ownFile` resource shows when the resource was created and when the state was updated. To fetch an `ownFile` resource, you'll need to use the id of the `file` resource, not the `fileAttachment` resource. ```js const ownFile = await sdk.ownFiles.show({ id: fileId }).data.data; ``` ```json "ownFile": { "id": { "uuid": "69fd8859-8380-49c0-ab7e-0ee502e40811" }, "type": "ownFile", "attributes": { "name": "File to be uploaded.pdf", "size": 10697, "state": "available", "createdAt": "2026-05-08T06:53:13.091Z", "stateUpdatedAt": "2026-05-08T06:53:25.880Z" } } ``` A `file` can be in one of several states: - pendingUpload - pendingVerification - available - verificationFailed Both `file` and `ownFile` resources are immutable, so updating an existing file resource is not possible. ## Visibility A file is first uploaded by creating an `ownFile` resource, which is visible to the creator of the resource. An `ownFile` can then be attached to another resource. Once a file has been attached to another resource, it becomes also visible as a `file` resource according to the scope of the corresponding fileAttachments attribute: - `publicFileAttachments` are visible to all users who have access to the resource in question - `protectedFileAttachments` are visible to the user who created the resource, and additionally the file can be revealed in a transaction to the other transaction participant - `privateFileAttachments` are visible only to the user who created the resource. For example, files attached to messages are visible to both participants of the transaction in question, because they are attached via the `publicFileAttachments` relationship of the message. You can review the [API reference for supported attaching resources](https://www.sharetribe.com/api-reference/marketplace.html#supported-attaching-resources) to see which resources have which scopes of file attachments available. In addition, operators have visibility to files and file attachments in Console. For example, files attached to messages are visible in the Console transaction details. ## Sharetribe storage for files Files are both uploaded to and downloaded from Sharetribe storage directly using pre-signed URLs – not using Sharetribe API endpoints. For uploads, this means that a user first creates the file resource in Sharetribe, and then fetches a time-limited signed URL connected to that specific file resource that allows uploading the file to Sharetribe storage. ```js const uploadDetails = sdk.fileUploads.create({ fileId }).data.data; ``` ```json "uploadDetails": { "id": { "uuid": "63abdec4-85e7-4157-8e99-2e2c7465d8be" }, "type": "fileUpload", "attributes": { "fileId": { "uuid": "69fd8859-8380-49c0-ab7e-0ee502e40811" }, "url": "https://unique-signed-sharetribe-upload-url.com", "headers": { "Content-Type": "application/pdf" }, "method": "PUT", "expiresAt": "2026-05-08T08:16:30.988Z" } } ``` For downloads, the user makes a POST request for a short-lived download URL for the file from Sharetribe API, and uses that generated URL to fetch the file from Sharetribe storage. There are two different download endpoints that use different ids, similarly to how files/show and own_files/show: - own_file_downloads/create (sdk.ownFileDownloads.create) for ownFile resources, where the parameter is the `file` id - file_downloads/create (sdk.fileDownloads.create) for file resources, where the parameter is the `fileAttachment` id ```js const downloadDetails = isOwnFile ? sdk.ownFileDownloads.create({ fileId }).data.data : sdk.fileDownloads.create({ fileAttachmentId }).data.data; ``` ```json "downloadDetails": { "id": { "uuid": "fd76299c-1be6-4250-8de6-10b1d8f9cb9e" }, "type": "fileDownload", "attributes": { "fileId": { "uuid": "69fd8859-8380-49c0-ab7e-0ee502e40811" }, "url": "https://unique-signed-sharetribe-download-url.com", "expiresAt": "2026-05-08T08:29:28.089Z" } } ``` Since both upload URLs and download URLs are short-lived, it's not possible to save them in a resource's extended data for reuse. Both resources have an `expiresAt` attribute that indicates how long they are valid. ## Security When a file is uploaded, the Sharetribe backend runs a security verification to ensure that the file is safe to handle and distribute, for example that it does not have malware. In addition, the backend verifies that the file mime type is acceptable and the size is less than 1GB. Blocked mime types include potentially harmful types such as shell scripts, executables, and disk images. Files can be attached to resources once they've been uploaded, even if they have not yet been verified. The Sharetribe backend processes the verification asynchronously, and the file becomes available if the verification succeeds. If you want to show the file as available immediately when verification succeeds, your client will need to poll the file to determine when it becomes available. --- ## Concepts Path: concepts/index.mdx # Concepts Explanations and background information for important concepts and design decisions behind the platform. ## Users & Authentication ## Listings ## Transactions ## Payments ## Pricing & Commissions ## Availability ## Development ## Content management ## Extended data ## API & SDK ## Messages & notifications ## Files ## Integrations --- ## Introduction to integrations in Sharetribe Path: concepts/integrations/integrations-introduction/index.mdx # Introduction to integrations in Sharetribe Integrations in Sharetribe refer to third-party solutions and tools that communicate with the Sharetribe marketplace to either retrieve information and take action outside Sharetribe, or trigger actions within Sharetribe. A lot of features that are not natively available in Sharetribe can be integrated with your marketplace solution with varying degrees of complexity. ## Sharetribe integration tools There are several ways to approach integrating third party tools with your Sharetribe marketplace. Your integration options within Sharetribe depend on the requirements of the third party tool you are integrating, and on the complexity of the integration you are looking to create. ### Client application The simplest integration tool is your client application. Since you have full control over your marketplace client app, you can add analytics and tracking tools such as [Hotjar](#hotjar-analytics) or [Goole Analytics](/template/analytics/how-to-set-up-analytics-for-template/#configure-google-analytics), or messaging tools such as [Sendbird](#sendbird-user-to-user-chat-app) or [Intercom](https://www.intercom.com/help/en/articles/168-install-intercom-on-your-website-to-support-and-onboard-logged-in-users), directly into your client app code. The Sharetribe Web Template includes a Node.js/Express server by default. Some third party integrations, such as [Voucherify](#voucherify-discount-coupons) or other promotion integrations, may require a secure context. For these types of integrations, you can use the template server to create endpoints or other logic in a secure way. ### Integration API If you want to create an integration where the third party solution listens to – or takes action on – your Sharetribe marketplace, you will likely need to use the [Integration API](/introduction/getting-started-with-integration-api/). Where the [Marketplace API](/concepts/api-sdk/marketplace-api-integration-api/#when-to-use-the-marketplace-api) is meant for everyday marketplace interactions, the Integration API gives operators powerful access to all marketplace data and activity. Through the Integration API, operators can enable third party tools to manage users, listings, transactions, bookings, stock reservations, and more. Due to its powerful nature, the Integration API should **only ever be used from a secure context**, such as your client application server or a separate backend application. Here, too, the template server is an excellent place to build your Integration API logic without revealing sensitive information to the public web in your browser client code. ### Events In addition to direct endpoints to marketplace resources, Integration API also exposes events. The majority of activity on your marketplace triggers events – users and listings created and updated, transactions transitioned, and so forth. You can review [the full list of supported event types](/references/events/#supported-event-types). By listening to events, you can build flows where third party solutions take action either in Sharetribe or within the solutions themselves. You can read more about [reacting to events in Sharetribe](https://www.sharetribe.com/docs/how-to/events/reacting-to-events/). ### Zapier Sharetribe has also built a [Zapier integration](https://www.sharetribe.com/help/en/collections/8975364) to facilitate integrating third-party tools with Sharetribe. The Zapier integration works by harnessing a subset of Sharetribe events and allowing operators to connect any of the other [5000+ tools and solutions](https://zapier.com/apps/) in the Zapier ecosystem. The Sharetribe Zapier integration makes it easier to take action in your favorite business tools, such as [Hubspot](https://zapier.com/apps/hubspot/integrations/), [Mailchimp](https://zapier.com/apps/mailchimp/integrations/) and others, based on your Sharetribe marketplace activity. The Sharetribe Zapier integration even allows you to [perform actions within Sharetribe](https://www.sharetribe.com/help/en/articles/8529989#h_7ccc6de359). ## Levels of integration The complexity of a Sharetribe integration with a third party tool can range from very simple to highly complex. One aspect determining the complexity is how intertwined the two tools need to be. On the simple end, adding a script to the client application for a tracking tool is a trivial change, and on the complex end a full third party payment gateway integration will likely take several weeks to implement. The full complexity of the feature also depends on the different use cases you need to cover on each side of the integration. For instance, if you want to integrate your Sharetribe marketplace with Shopify and synchronize listing stock between the two platforms, you will need to cover several use cases on both sides, such as adding, updating and removing listings, as well as handling purchases, returns and disputes. For illustration, here are some examples of types of integrations with different levels of complexity: **Simple integrations:** - Calling third party APIs and SDKs from your client application - use cases: uploading files to external storage, adding a messaging tool - Adding a third-party script to your client application - use cases: integrating analytics providers - Adding a custom endpoint in your client app server that calls a third party API and then updates information in Sharetribe using the Marketplace API or the Integration API - use cases: enabling downloadable content in listings - Listening to Sharetribe events using Zapier and taking action in another Zapier-connected application - use cases: sending a text message of a new booking to providers using Twilio **More complex integrations** - Listening to Sharetribe events using your own polling tool and taking action within Sharetribe - use cases: partial or complete payment gateway integrations - Synchronizing Sharetribe and a parallel system to update each other when any changes happen in either solution - use cases: calendar integration, Shopify ## Examples of specific integrations ### Hotjar analytics [Hotjar](https://www.hotjar.com/) is an analytics tool for websites that allows you to collect user activity heatmaps, feedback, and user behavior recordings, among other features. - Add a [tracking script to the client application](/template/analytics/how-to-set-up-analytics-for-template/#custom-analytics-libraries). - Add custom CSP directives to [/server/csp.js](https://github.com/sharetribe/web-template/blob/main/server/csp.js). ### Mailchimp email list [Mailchimp](https://mailchimp.com/) is an email marketing and automations tool that you can use to communicate directly with your marketplace users. Since Mailchimp uses Zapier, integrating Sharetribe and Mailchimp is straightforward: - Use the [sample Zapier template](https://zapier.com/shared/add-a-new-user-in-your-marketplace-to-a-mailchimp-audience/412d7744a23855ce00941567a619c7ffb7652335) for adding new users to your Mailchimp mailing lists. ### Twilio SMS [Twilio](https://www.twilio.com/) is a communications platform that offers multiple channels of communicating with your marketplace users, including SMS and WhatsApp messages, calls and video. Twilio has a Zapier integration, so you can use Zapier to trigger actions in Twilio tools with no coding. - Follow the [Sharetribe tutorial](https://www.sharetribe.com/help/en/articles/8788994) to send providers a SMS message whenever their listing gets booked. ### Sendbird user-to-user chat app In Sharetribe, messages between users are always related to transactions. If you want to allow more complex messaging flows between users, you can integrate a user-to-user chat tool such as [Sendbird](https://sendbird.com/). The initial integration is fairly straightforward to implement, and the eventual complexity depends on the use cases you want to cover. - Create an account in Sendbird. - Add the Sendbird [UI kit](https://sendbird.com/docs/uikit/v3/react/overview) and [SDK](https://sendbird.com/docs/chat/v4/javascript/overview). - Implement and stylise a UI chat component. - Initialise a Sendbird user id for the user when they sign up to Sharetribe, and save it in the user's private data so it can be used whenever they log in. ### Voucherify discount coupons [Voucherify](https://www.voucherify.io/) is a promotions tool that allows you to create, validate and redeem discount coupons, gift cards, automatic discounts and other promotional offers. The initial integration has a number of steps but is still fairly straightforward to implement, and the eventual complexity depends on the use cases you want to cover. - Create an account in Voucherify and create your own codes or use the sample codes. - Add [Voucherify SDK](https://docs.voucherify.io/docs/sdks). - Create a helper file in the server that contains functions to validate and redeem a code using the Voucherify SDK. - Add an input element to the order panel for the discount code. - Pass the discount code to the line item calculation and to CheckoutPage (in a similar way to [this pricing tutorial](/tutorial/customize-pricing/)). - Validate the code in endpoints for _transaction-line-items_, _initiate-privileged_ and _transition-privileged_, and pass the validation result to _server/api-util/lineItems.js_. - Add a logic to _server/api-util/lineItems.js_ that adds a discount line item with the correct percentage or amount if the code is valid. You will need to decide whether the discount line item is included - for both customer and provider, in which case the provider receives a decreased payout, - or only for the customer, in which case the marketplace receives a decreased commission – in this case, the discount cannot be greater than the marketplace commission from customer and provider combined. - Only redeem the code once i.e. when actually initiating or transitioning the transaction with Marketplace API. ### Google Calendar synchronised with Sharetribe availability calendar If you run a booking marketplace, you may want to allow providers to synchronize their own [Google Calendar](https://developers.google.com/calendar) to their listing's Sharetribe availability calendar. That way, providers do not need to add availability exceptions manually to Sharetribe, and they will be able to see any bookings in their own calendar without needing to visit the marketplace. Like all two-way integrations, this is a complex one, and you will need to specify the use cases you want to cover to determine the full complexity of the integration. - Google provides [Calendar API](https://developers.google.com/calendar/api) to modify calendar events. - You can set up [push notifications](https://developers.google.com/calendar/api/guides/push) to track changes on the Google side. - Authenticate your Sharetribe app to get information and make changes on behalf of the user in Google Calendar. - Whenever a booking gets created or updated in Sharetribe, update the provider's Google Calendar accordingly. You can do this either by - creating custom endpoints in your client app server that call the Sharetribe APIs as well as the Calendar APIs, - or you can listen to Sharetribe events and call the Calendar APIs on relevant events. - You can [watch the provider's Google Calendar](https://developers.google.com/calendar/api/v3/reference/events/watch), and whenever a new event is created or an existing one is removed, create or remove an [availability exception](https://www.sharetribe.com/api-reference/integration.html#availability-exceptions) in Sharetribe to prevent other customers from booking that specific time slot. ### Payment gateway integration Integrating a third-party payment gateway, such as [Paypal Commerce Platform](https://www.paypal.com/us/business/platforms-and-marketplaces) or [Stripe Billing](https://stripe.com/en-gb/billing), is a complex and extensive integration. Even when you follow our high-level guide on integrating a payment gateway, you will need to do a lot of your own investigating and testing, as well as collaborate closely with the support team of the payment gateway provider you have chosen. - Contact the support team of the payment gateway tool you intend to use, and make sure you are eligible for their service. Some providers may have limitations regarding platform size or geography. - Get familiar with our how-to article on [integrating a third-party payment gateway to Sharetribe](/how-to/payments/how-to-integrate-3rd-party-payment-gateway/). --- ## How the listing search works Path: concepts/listings/how-the-listing-search-works/index.mdx # How the listing search works In Sharetribe, listings are searched by using the [/listings/query](https://www.sharetribe.com/api-reference/marketplace.html#query-listings) endpoint. Sharetribe has a powerful listing search engine, which can find listings based on multiple criteria. This article provides an overview of those criteria. See the API reference documentation for a full list of available search parameters for the following endpoints: - [Marketplace API listings/query](https://www.sharetribe.com/api-reference/marketplace.html#query-listings) - [Marketplace API own_listings/query](https://www.sharetribe.com/api-reference/marketplace.html#query-own-listings) - [Integration API listings/query](https://www.sharetribe.com/api-reference/integration.html#query-listings) Do note that for all search parameters created in custom code (i.e. outside Console configurations), you will need to create a [search schema](/how-to/search/manage-search-schemas-with-sharetribe-cli/) so that the data is indexed correctly for search. ## Keyword search The keyword search matches the user's search query against the listing's title, description, and `text` type metadata and public data fields with a defined schema. Because descriptions can be toggled off as a requirement per listing type, some listings may have 'hidden' descriptions if this setting changed after the listing was created. The description is still indexed for search and factored into the relevance score. The keyword search works so that it matches the `keywords` parameter to text content of a listing. The listing attributes that are matched in keyword search by default are _title_ and _description_. Listing public data fields and metadata fields can also be used in the keyword search by defining a `text` type [search schema](/references/extended-data/#search-schema). All `text` type public data and metadata fields with a defined schema are indexed for the keyword search. When the listing text content is indexed, in addition to indexing the actual word, it is also broken into subwords, _n-grams_. For words longer than 3 characters, n-grams of 3 and more characters are constructed from the beginning and end of the word. So if an indexed field contains the word _local_ in addition to the actual word the following n-grams are indexed: _loc_, _loca_, _ocal_, and _cal_. The search results are sorted so that a match with an actual word from the listing always weighs more than a match with an n-gram. The order in which matches in different fields of a listing effect the search score is the following: _title > description > extended data fields_. However, the search score differences between the fields are substantially smaller than the difference between a match with an actual word in the listing and an n-gram. Listings that don't match the search keywords at all are not included in the results. The `keywords` parameter is a single string that is tokenized on non-alphanumeric characters. Therefore, passing _local attractions_ as a value for the parameter will conduct a search with two keywords: _local_ and _attractions_ and those are then matched against the listing content. A listing will be included in the results, in case any of the keywords match. If you are using Console configurations, you can configure your marketplace to use keyword search in Console. Read more in our Help Center: - [Listing search options](https://www.sharetribe.com/help/en/articles/8413316-listing-search-options) ## Location search The search can be used to display only listings that are within a provided radius from certain coordinates. The listings search provides two parameters to modify the search by location: `origin` and `bounds`. `origin` parameter takes a single location. It does not limit the results in any way but it sorts the results from closest to the furthest from the given point. The `origin` parameter can not be combined with `keywords`. In order to combine location and keyword search, the `bounds` parameter can be used. It takes north-east and south-west corners of a bounding box that limits the returned results to that area. It does not affect the sorting of results. The bounds can then conveniently be used to match search results to a map view. The Sharetribe Web Template uses `bounds` search by default. You can also take the `origin` parameter to use in [src/config/configMaps.js](https://github.com/sharetribe/web-template/blob/main/src/config/configMaps.js#L39-L45) if you are not using keyword search. ## Custom filter search with extended data In Sharetribe, only top-level extended data attributes can be indexed i.e. used for search. If you have a public data attribute where the value is a JSON object with nested attributes, it is not possible to create a search schema for the attribute. So instead of using a nested attribute: ```jsx publicData: { instrumentProficiency: { // These attributes cannot be indexed for search in Sharetribe violin: 'professional', guitar: 'intermediate', tuba: 'beginner', }, } ``` you would need to set all attributes you want to query as top-level attributes: ```js publicData: { // These attributes can be indexed for search in Sharetribe violinProficiency: 'professional', guitarProficiency: 'intermediate', tubaProficiency: 'beginner', } ``` This allows you to search for each attribute. ```jsx sdk.listings.query({ pub_violinProficiency: 'professional', pub_guitarProficiency: ['beginner', 'intermediate'], }); ``` You would then index the attributes by adding a search schema according to the data type of the extended data attribute: - String values can be indexed for keyword search using **schemaType: text** or **schemaType: shortText**, or for enum search, i.e. searching with the attribute name and exact values, using **schemaType: enum** - Arrays of strings can be indexed for multi-enum search, i.e. searching with the attribute name and exact values, using **schemaType: multi-enum** - Integers can be indexed for numeric search with start and end ranges using **schemaType: long** - Booleans can be indexed for boolean search using **schemaType: boolean** Schema type **multi-enum** supports two types of query semantics: **has_all** i.e. AND semantics, and **has_any** i.e. OR semantics. The Sharetribe Web Template uses the default **has_all** querying when a multi-enum attribute is configured in Console. To configure a **has_any** attribute, you can follow the instructions in this article: - [Extend listing data in Sharetribe Web Template](/how-to/listings/extend-listing-data-in-template/) ## Price search Prices can be searched with a single value, or with **start** and **end** parameters: - **listings/query?price=2000** returns listings with the exact price of 2000 minor units of the listing's currency (e.g. USD 20) - **listings/query?price=2000,10000** returns listings with a price between 2000 and 10000 minor units of the listing's currency (e.g. USD 20 - USD 100) - **listings/query?price=2000,** returns listings with a price at or above 2000 minor units of the listing's currency (e.g. USD 20) - **listings/query?price=,10000** returns listings with a price below 10000 minor units of the listing's currency (e.g. USD 100) The price filter does not consider the listing's currency. This means that if you develop a multi-currency marketplace, listings costing e.g. USD 20 and HKD 20 will be returned with the same query. ## Availability search Listings can be queried by their availability for booking. Listings can have either day-based or time-based availability, so availability filtering also has day-based and time-based options. - Day-based availability filtering matches listings with day-based availability plans. The corresponding query parameters are **day-full** and **day-partial**. - Time-based availability filtering matches listings with time-based availability plans. The corresponding query parameters are **time-full** and **time-partial**. Queries using time-based availability filtering do not support pagination. All bookable listings created with the Sharetribe Web Template use time-based availability plans by default. They therefore require time-based availability filtering, regardless of the unit type specified in the listing type. Full availability queries, i.e. **day-full** and **time-full**, match listings that have the specified availability for the full duration of the queried time. In the example below, a listing would be returned if it has at least three seats available for the full duration between the start and end time points. ```jsx sdk.listings.query({ start: '2026-02-17T08:00:00.000Z', end: '2026-04-17T15:00:00.000Z', availability: 'time-full', seats: 3, }); ``` Partial availability queries, i.e. **day-partial** and **time-partial**, match listings that have the specified seats available for the specified minimum duration (days in **day-partial**, minutes in **time-partial**) at any point during the queried time. Partial queries using **minDuration** do not support pagination. In the example below, a listing would be returned if it has at least three seats available for at least one hour at some point between the start and end time points. ```jsx sdk.listings.query({ start: '2026-02-17T10:00:00.000Z', end: '2026-04-17T10:00:00.000Z', availability: 'time-partial', seats: 3, minDuration: '60', }); ``` In availability queries, the **start** and **end** moments must be at most 90 days apart. So to query intervals of more than 90 days at a time, you will need to combine results from multiple API calls. | Availability filtering type | **day-full** | **day-partial** | **time-full** | **time-partial** | | ------------------------------------------ | ------------ | --------------- | ------------- | ---------------- | | Match listings with availability plan type | day | day | time | time | | Availability match type | full | partial | full | partial | | Unit of **minDuration** parameter | – | days | - | minutes | | Used in the Sharetribe Web Template | no | no | yes | yes | | Supports result pagination | yes | no | no | no | Read more in the API reference: - [Marketplace API listings/query availability filtering](https://www.sharetribe.com/api-reference/marketplace.html#availability-filtering) ## Stock search You can filter your query to only return listings with positive stock using the **minStock** parameter. ```jsx sdk.listings.query({ minStock: 1 }); ``` This query will only return listings where stock is defined and the value matches or exceeds **minStock**. This is because the API uses **stockMode: strict** by default. On a marketplace with multiple listing types, you may also want to show listings that do not have defined stock, such as booking or free messaging listings. In that case, you can explicitly set the **stockMode** parameter to **match-undefined** alongside **minStock**: ```jsx sdk.listings.query({ minStock: 1, stockMode: 'match-undefined' }); ``` ## How about user search? The Sharetribe Marketplace API does not have an endpoint for querying users. This is because listings are modeled as the focus of the marketplace. If you do, however, want to implement a search functionality for users, you have a few options. ### Users as listings When modifying your listing creation flow, you can model the listings as service provider profiles. This means that the user entity is distinct from the user's service provider entity. You can then use the default listing search to query the service provider profiles with all the query options described above. This article in our Help Center has more detailed instructions on the option of using listings as user profiles: - [How to configure listings to look like user profiles](https://www.sharetribe.com/help/en/articles/12953405-how-to-configure-listings-to-look-like-user-profiles) ### Custom user search endpoint in the template server The Sharetribe Integration API does have an endpoint for querying users. However, [using the Integration API safely requires a secure environment](/concepts/api-sdk/marketplace-api-integration-api/#when-to-use-the-integration-api), such as the template application server. Never use Integration API from browser code! Integration API can only be securely used in server environments. In addition, the Integration API [/users/query](https://www.sharetribe.com/api-reference/integration.html#query-users) endpoint returns not only public user information but also non-public information. This means that if you do want to use the Integration API user query, you need to create a custom server endpoint in the template's server that calls the Integration API endpoint and **only returns the user's public information** – public data and metadata – back to the browser. Otherwise, you risk revealing sensitive user information to everyone who visits your marketplace site. You can see all of the available search parameters for the endpoint in our API reference: - [Integration API users/query](https://www.sharetribe.com/api-reference/integration.html#query-users) ## Sorting your search results Listing sorting order can be customized per query. When a query uses **keyword** and **origin** parameters, the results are sorted based on those attributes as described above. When using the **sort** parameter, sorting is supported by one or more of the following attributes: - listing price, - listing creation time, - or any **numeric** attribute in the listing's public data or metadata. The **sort** parameter supports up to three parameters in a single query. --- ## Listings in Sharetribe Path: concepts/listings/listings-overview/index.mdx # Listings in Sharetribe On a Sharetribe marketplace, listings represent the items, facilities or services that users buy and sell or book and offer. Using extended data, listings can be modified to cover several different kinds of marketplaces. ## Listing lifecycle Between being created and deleted, listings go through several different states. Depending on the listing's state, they are either returned with the public Listing endpoints or they are only available to the listing's author. When tracking [listing events](/references/events/#supported-event-types) through the [Integration API](https://www.sharetribe.com/api-reference/integration.html), it is important to also pay attention to the state of the listing. For instance, in the default Sharetribe Web Template implementation, the listing is created as draft, and then goes through several updates in the draft state before it is published. In other words, to catch the correct listing event, it is useful to filter by both event type and listing state. ### Draft You may want to allow users to create listings as drafts first, and then publish them separately. To do that, you would create the listing in **state: draft**. Draft listings are not returned by the [listings/query](https://www.sharetribe.com/api-reference/marketplace.html#query-listings) endpoint, but they are visible to the author and through Sharetribe Console. Draft listings can be published using the [own_listings/publish_draft](https://www.sharetribe.com/api-reference/marketplace.html#publish-draft-listing) endpoint, and they can be discarded using the [own_listings/discard_draft](https://www.sharetribe.com/api-reference/marketplace.html#discard-draft-listing) endpoint. ### Pending approval If the marketplace [requires listing approval for new listings](https://www.sharetribe.com/help/en/articles/8413545-approve-listings-before-publishing), a draft listing does not get published directly when the [own_listings/publish_draft](https://www.sharetribe.com/api-reference/marketplace.html#publish-draft-listing) endpoint is accessed. Instead, it moves into **state: pendingApproval**. The operator can then approve the listing in multiple ways: - in Sharetribe Console - through the [listing approval Integration API endpoint](https://www.sharetribe.com/api-reference/integration.html#approve-listing) - [using our Zapier integration](https://www.sharetribe.com/help/en/articles/9914794-zapier-tutorial-approve-a-listing-after-the-listing-fee-is-paid) The latter two options are useful if the approval is dependent on e.g. an extended data attribute on the listing or author, or on the author paying a fee to the marketplace to publish the listing. A use case for this would be e.g. a marketplace where non-paid members can publish one listing and paid members can publish unlimited listings – an integration could check whether the user has a `premium: true` flag in their metadata and approve the listing accordingly. ### Published Published listings are returned by [listings/query](https://www.sharetribe.com/api-reference/marketplace.html#query-listings) and [listings/show](https://www.sharetribe.com/api-reference/marketplace.html#show-listing) endpoints. To directly create a published listing instead of a draft, you would need to use the [own_listings/create](https://www.sharetribe.com/api-reference/marketplace.html#create-listing) endpoint. Creating a published listing will follow the pending approval rules of the marketplace – if listing approval is required, a listing created with **own_listings/create** will be in **state: pendingApproval** instead of **state: published**. After a listing has been published, it can still be modified by the author. Even if the marketplace has listing approval enabled, modifying listings after they are published does not set them back to the **pendingApproval** state. This means that the listing approval feature can not fully be used to moderate e.g. listing content, but rather the number of listings each user has active on the marketplace. ### Closed A listing can be closed by both the author and the marketplace operator. A closed listing is not returned by the [listings/query](https://www.sharetribe.com/api-reference/marketplace.html#query-listings) and [listings/show](https://www.sharetribe.com/api-reference/marketplace.html#show-listing) endpoints, but it is still returned by the [own_listings/query](https://www.sharetribe.com/api-reference/marketplace.html#query-own-listings) and [own_listings/show](https://www.sharetribe.com/api-reference/marketplace.html#show-own-listing) endpoints, i.e. it is visible to the author. Both author and marketplace operator can also [open the listing](https://www.sharetribe.com/api-reference/marketplace.html#open-listing) after it has been closed i.e. set its state back to published. However, if the marketplace [restricts listing posting rights](https://www.sharetribe.com/help/en/articles/9503118-restrict-listing-posting-rights) and the author's posting rights have been revoked, the author cannot open closed listings. ### Deleted Listings in Sharetribe can only be deleted by the operator in Sharetribe Console. In addition, if the author's user account is deleted, all their listings are marked deleted as well. Listing deletion is permanent. Deleted listings don't get returned by any listing endpoints. If a marketplace user has transactions related to a deleted listing, however, the deleted listing can be returned as an included related resource with no other data than the listing id and `deleted: true`. ## Listing extended data Listings are one of the resource types in Sharetribe where you can add [extended data](/references/extended-data/). This means that listings can have custom attributes beyond the default ones that Sharetribe offers. Extended data can be managed through Marketplace API [own_listings](https://www.sharetribe.com/api-reference/marketplace.html#own-listings) and Integration API [listings](https://www.sharetribe.com/api-reference/integration.html#listings) create and update endpoints, as well as in Sharetribe Console for individual listings. Two example use cases for listing extended data are custom search and differentiating between listing types. However, listing extended data is a powerful feature to customise listing behavior in Sharetribe, so there are multiple use cases that can be solved with setting attributes in listing extended data and then either passing those attributes as query parameters or managing the client application behavior based on the attributes. ### Custom listing search One of the most powerful features of Sharetribe is the listing search using the [listings/query](https://www.sharetribe.com/api-reference/marketplace.html#query-listings) endpoint. By default, you can query by price, [keywords, origin,](/concepts/listings/how-the-listing-search-works/) bounds, availability, and stock, among others. In addition to the default parameters, you can query listings by public extended data attributes, i.e. public data and metadata. You will need to [set a search schema](/how-to/search/manage-search-schemas-with-sharetribe-cli/) for the attribute so that it is indexed for search within Sharetribe, unless the attribute has been defined as a listing field in Console. ### Different types of listings In addition to search filtering, you can also build views that filter listings by type. You can for instance create service listings with [availability and bookings](/references/availability/), as well as product listings with [stock](/references/stock/), in the same marketplace, and differentiate them with a **listingType** attribute in listing extended data. With both availability and stock listings in the same marketplace, you likely want to have separate [transaction processes](/concepts/transactions/transaction-process/), whose information is also saved in the listing extended data. By passing listing type as a query parameter to the endpoint, you can then create differentiated views for the two types of listings. Again, using extended data as query parameters requires [setting a search schema](/how-to/search/manage-search-schemas-with-sharetribe-cli/) if the field does not have a Console-created search schema by default. Some marketplaces want to allow filtering by entities that are not strictly speaking listings available to be purchased. Examples include searching by service provider, or searching for storefronts to see what listings they offer. In these kinds of cases, it is useful to model those elements as a new type of listing, and then determine their behavior in the client app based on the extended data attribute. For instance, for a service marketplace, you could model the service provider listings as bookable profiles. You could also create a searchable storefront listing that cannot itself be booked, and instead it displays all the listings from that specific listing author and allows users to contact the store. In Sharetribe, listings can be modeled and modified to cover a range of different use cases. If you are wondering about a use case for listings in Sharetribe, do reach out to Sharetribe Support through the chat widget in your Sharetribe Console and let us know. We'll be happy to help you figure out your specific use case and give you some suggestions for implementation. --- ## Email notifications Path: concepts/messages-notifications/email-notifications/index.mdx # Email notifications Sharetribe sends email notifications to users when specific events occur in your marketplace. There are two categories of email notifications in Sharetribe: built-in email notifications, which relate to user account management, and transaction process emails, which get triggered at specific stages of transactions. For instance, an built-in email notification gets sent when a user changes their email or password or when they need to verify a new email address. A transaction process email can inform the user of a successful payment or a new booking request. ## Enable email notifications Email notifications are automatically enabled in your test and dev marketplaces. However, in your Live marketplace, you must [configure outgoing email settings](/how-to/emails-and-notifications/set-up-outgoing-email-settings/) for email notifications to work. Users will not receive email notifications until they have verified their email address. Sharetribe does not send emails to unconfirmed addresses to avoid people flagging those as spam emails, as that can hurt your marketplace's ability to send mail to legitimate users. You can disable some email notifications through Console. ## Built-in email notifications There are ten built-in email notifications, related to end-users' account management, permissions, and listings approval. Use the [Console](https://console.sharetribe.com/) to manage the built-in email notifications. You can edit the built-in email content with the Email texts editor in Console > Build > Content > Email texts. You can preview and customise built-in emails using the [Built-in email notifications editor](https://console.sharetribe.com/advanced/email-notifications) in the Sharetribe Console. You can find the editor in Console under the Build > Advanced > Email notifications section. These built-in email notifications can be disabled through Console: - Listing approved - New message - User approved - User joined - User permissions changed - Verify email address The email templates use the [Handlebars template language](/references/email-templates/#handlebars). The most prominent use of the handlebar templates is the `t` helper, which is used to render the template content. The helper renders either the message denoted by a message key, or a fallback message. ``` {{t "ResetPassword.MembershipParagraph" "You have received this email notification because you are a member of {marketplaceName}. If you no longer wish to receive these emails, please contact {marketplaceName} team." marketplaceName=marketplace.name}} ``` In each template, you can use a set of predefined context variables (such as the name and email of the recipient). You can find all context variables to the right of the built-in email template editor. You can access user extended data through the context variables, if you want to customise email content further. You can edit the text content of email notifications in Console > Build > Content > Email texts. You can preview the built-in emails, and customise their structure,using the [Built-in email template editor](https://console.sharetribe.com/email-templates) in the Sharetribe Console. You can find the editor in the Console under the Build > Advanced section. The built-in email template editor does not include a visual editor, but if you want, you can design your email in any [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG) email editor you find online and paste the resulting HTML into the built-in email editor. You can then preview the email by sending it to your email address by clicking on "preview" and pressing on the "Send a test email" button. For more information on how to use the Handlebars to customise email templates, see our [reference article on email templates](/references/email-templates/#handlebars). ## Transaction notifications Transaction notifications inform the user of events related to the [transaction process](/concepts/transactions/transaction-process/). These notifications usually relate to information about bookings and payments, in contrast to built-in email notifications, which are typically actionable and related to account management. You can edit the content of the transaction notifications in Console > Build > Content > Email texts. You can preview your changes, as well as update message keys and add or delete transaction notifications, with the [Sharetribe CLI](/how-to/transaction-process/edit-transaction-process-with-sharetribe-cli/). The [template sub-directory](https://github.com/sharetribe/example-processes/tree/master/default-booking/templates) in the transaction process directory contains all the transaction notification email templates. All transaction notifications use the [Handlebars templating language](/references/email-templates/#handlebars) and can be edited similarly to built-in email templates. In addition to making changes to the content of the transaction notifications, you can change [when email notifications get sent](/references/transaction-process-time-expressions/). A transaction notification must always be associated with a specific transition. When a specific transition transitions, the transaction notification associated with it is triggered. Read more about transaction notifications in our [tutorial on how to add new email notifications](/tutorial/add-email-notification/). ## Custom notifications through Zapier Sometimes the built-in and transaction notifications are not enough, and you might need more control over what triggers an email. Examples include notifying your marketplace operators when a user submits a listing for review or sending a listing author an email once their listing is published. As neither of these actions is transaction related, you can not trigger them as transaction notifications. Instead, you must listen to events and trigger an email to respond to the correct event. For building custom email notifications, we recommend connecting your app to Zapier. You can use Zapier to listen for events in your marketplace and react to them using different actions. Zapier also supports sending text messages instead of emails. Read more about Zapier in our [Help Center](https://www.sharetribe.com/help/en/collections/8975364). If you are unsure how to approach a Zapier integration, do not hesitate to reach out to our support team through one of our [official support channels](https://www.sharetribe.com/help/en/). We will be happy to help you figure out your specific use case and give you some suggestions for implementation. --- ## Messages Path: concepts/messages-notifications/messages/index.mdx # Messages Messages let your users communicate with other users in your marketplace. They can be exchanged freely between a customer and a provider once they have engaged in a transaction. Messages always need to be associated with a transaction and can not be sent outside of one. The default booking and purchase [transaction processes](https://github.com/sharetribe/example-processes/blob/master/default-booking/process.edn) include an inquiry transition, which initiates a transaction without running any [actions](/references/transaction-process-actions/#actions), allowing the provider and customer to send messages to each other. In addition, the [default-inquiry process](https://github.com/sharetribe/example-processes/blob/master/default-inquiry/process.edn) initiates a simple transaction that is only intended for messaging. Note that messages do not alter the transaction or transition it to a different state. ## Sending messages You can send messages using the [send message endpoint](https://www.sharetribe.com/api-reference/marketplace.html#send-message), which requires an authenticated user’s access token to call. The [Integration API](/introduction/getting-started-with-integration-api/) does not offer an endpoint to send messages, and therefore, only authenticated users can send messages through the Marketplace API. Allowing operators to send messages, or more than two users to participate in a conversation, would require integrating a third-party tool to replace the default Sharetribe messaging system. ### Marketplace leakage and sanitizing content in sent messages On some marketplaces, operators may want to prevent users from sharing phone numbers or other contact information. Sharetribe does not currently have a built-in mechanism to configure this in Console. However, you can modify the message sending logic in your custom code to check the message content for unwanted elements, and then replace that content with a sanitized version before passing it to the `messages.send` endpoint. It's good to note that if you block certain content, users might find ways to circumvent your technical measures, so it is good to also [implement other mechanisms to prevent marketplace leakage](https://www.sharetribe.com/academy/how-to-discourage-people-from-going-around-your-payment-system/). ## Querying messages You can query messages through the query messages endpoint, which returns all messages in a given transaction. Messages can also be included as a relationship when [querying transactions](https://www.sharetribe.com/api-reference/marketplace.html#query-transactions). ## Email notifications New messages trigger a [built-in email notification](/concepts/messages-notifications/email-notifications/) sent to the receiving party of the message. You can edit built-in email notification content through [Console > Build > Content > Email texts](https://console.sharetribe.com/a/content/email-texts), and you can modify the structure and code of the notifications through [Console](https://console.sharetribe.com/email-templates/new-message). ## Zapier, events and messages Using [Zapier](https://www.sharetribe.com/help/en/collections/8975364) you can connect your marketplace with other web applications and create automated workflows. Even though you can’t listen for new messages through Zapier, messages can easily be retrieved as a transaction relationship. For more complex customisations, you can use events to listen to new or deleted messages. ### How to retrieve messages in Zapier You can use Zapier to access messages using the transaction ID that is associated with them. By default, when you listen to transaction events, the message relationship is not included. To include the message relationship, add the action "Show Transaction" to the trigger "Transaction events" and select messages from the dropdown menu. From the dropdown menu, you can select messages, and you are then able to use the message content in your Zap. ### Events and messages Listening to [events](/how-to/events/reacting-to-events/) through the [Integration API](/introduction/getting-started-with-integration-api/) is the most versatile way to react to what is happening in your marketplace. As sending new messages does not affect transaction state or transitions, you can’t use Zapier to detect new messages as it can only react to transactions, listing and user events. Events allow you to listen to [created messages](/references/events/#supported-event-types) and react directly to them. See how to [react to events](/how-to/events/reacting-to-events/) and the [Integration API example script repository](https://github.com/sharetribe/integration-api-examples) if you’re unsure where to start building your integration. ## Notification count By default, Sharetribe Web Template renders a notification symbol when there are transactions that require action from the user: - For bookings, when the user is a provider and needs to accept the booking - For purchases, when the user is a provider and needs to deliver the purchase - For negotiations, when the user needs to take action on the current step of the negotiation ![Notification symbol](./notification.png 'Notification symbol') This is how the default logic works: {

The template fetches transactions that require attention

} The template [makes two API calls](https://github.com/sharetribe/web-template/blob/main/src/ducks/user.duck.js#L118-L144) that retrieves all sales transactions (i.e. transactions where the current user is the provider) that are in a state that [requires provider attention](https://github.com/sharetribe/web-template/blob/main/src/transactions/transaction.js#L354), and all order transactions (i.e. transactions where the current user is the provider) that are in a state that [requires customer attention](https://github.com/sharetribe/web-template/blob/main/src/transactions/transaction.js#L367). {

The template renders notification count

} The number of relevant sales and order transactions determines the [notification count](https://github.com/sharetribe/web-template/blob/main/src/containers/TopbarContainer/TopbarContainer.js#L53) that in turn determines whether to show [the notification indicator](https://github.com/sharetribe/web-template/blob/main/src/containers/TopbarContainer/Topbar/TopbarDesktop/TopbarDesktop.js#L43). The variables [currentUserSaleNotificationCount and currentUserOrderNotificationCount](https://github.com/sharetribe/web-template/blob/main/src/ducks/user.duck.js#L302-L303) store the number of active notifications. ### Customize the notification logic You can extend the messaging logic in many ways. For example, a common customisation is to display a notification every time a user receives a new message. Instead of using the default logic, you could use an extended data attribute stored in the user object to store the number of unread notifications. This attribute could be updated every time a new message is detected using [events](/how-to/events/reacting-to-events/). --- ## Automatic off-session payments in transaction process Path: concepts/payments/off-session-payments-in-transaction-process/index.mdx # Automatic off-session payments in the transaction process In a typical transaction, the customer pays upfront, but the money is held until the transaction completes (e.g. until the booking end date) before it is paid out to the provider's bank account. Normally, the maximum amount of time the money can be held when using Stripe payments is 90 days. Therefore, the maximum amount of time customers can book in advance is limited, if your transaction process follows this payment pattern. Sharetribe API has capabilities for [saving payment card details for future use](https://www.sharetribe.com/api-reference/marketplace.html#stripe-customer). In addition, it is possible to configure your transaction process in such a way that the customer is charged automatically off-session at a certain point in time (i.e. when they are not present and interacting with your web site or app), provided that they have saved a payment card to their account. This allows you to charge customers closer to their booking dates, so that the money can be held in Stripe throughout the booking period. Another way to use the off-session payment logic might be to create a pre-order functionality for a product marketplace. In a pre-order transaction flow, the provider could put their future offerings for sale and include a delivery date well into the future. Customers could then pre-order the items, and their card would only be charged once the provider marks the orders as being in progress or shipped. Again, this would allow the pre-order time to be longer than the Stripe limitation of 90 days. ## Transaction process example Suppose your sauna rentals marketplace should allow customers to book saunas up to a year in advance, but the customer is only charged at a specified moment before the booking. The figure below illustrates how a part of your transaction process might look like. The [example-processes Github repository](https://github.com/sharetribe/example-processes/) contains [an example transaction process](https://github.com/sharetribe/example-processes/tree/master/automatic-off-session-payment) corresponding to the flow described. ![Example transaction process with delayed payment](./tx-delayed-payments.png 'Example transaction process with delayed payment') In this example, a transaction goes as follows: 1. The customer picks the desired booking dates and initiates a transaction. The price of the transaction is calculated, but no preauthorization or payment is made at this point. 2. The provider verifies the request and accepts the booking. 3. At a later point in time (1 month before the booking start time in this example, and 1 day before the booking start in the [example process](https://github.com/sharetribe/example-processes/tree/master/automatic-off-session-payment)), an attempt is made to automatically charge the customer's stored payment card. If the charge succeeds, the transaction continues onwards. 4. The automatic charge can fail for multiple reasons. If the charge fails, the customer (and optionally also the provider) receives an email notification and the customer is asked to visit the marketplace website in order to pay manually.
How does creating and capturing an off-session payment work? In the auto-payment transition, the payment intent creation needs to be configured to use the customer's saved payment information, if it exists. When the action is configured like this, it both creates and confirms the payment intent. Therefore, only capturing the payment intent remains necessary. ```clojure {:name :transition/auto-payment, :from :state/pending-payment, :to :state/paid, :at {:fn/plus [{:fn/timepoint [:time/first-entered-state :state/pending-payment]} {:fn/period ["PT5M"]}]}, :actions [{:name :action/stripe-create-payment-intent, :config { :use-customer-default-payment-method? :true }} {:name :action/stripe-capture-payment-intent}]} ```
It is important to note that an off-session payment can fail for various reasons. For instance: - the card could be denied due to insufficient funds, - the issuing bank may require additional authentication from the customer (this can easily occur with European payment cards with [Strong Customer Authentication regulation](/template/payments/strong-customer-authentication/)) - or the payment card might have expired. It is therefore always important to allow for a fallback payment path in your transaction process. In Sharetribe, only one transition from a state can be triggered automatically, so a fallback payment path must trigger upon a user action, as in the example. You can build upon this example and extend it to make the payment process more robust. For instance, in case the customer fails to pay for the transaction within certain amount of time, you may wish to allow the provider or marketplace operator to cancel the transaction, or allow the provider to post a review of the customer. In addition, you may consider disallowing customers to remove their stored payment card in your UI implementation, if they have ongoing transactions for which they have not yet been charged. ## Considerations about implementation in Sharetribe Web Template If you want to implement [the example process](https://github.com/sharetribe/example-processes/tree/master/automatic-off-session-payment) in your user interface, there are multiple ways to do so. If your user interface is based on the Sharetribe Web Template, here are a few things worth considering. ### Transitions and states [Transitions and states](/tutorial/create-transaction-process/#update-client-app) are used in the template as conditions for several behaviors, including redirects and displayed content. The [transaction resource](https://www.sharetribe.com/api-reference/marketplace.html#transaction-resource-format) contains information about the transaction's last transition and its timestamp. ### Separating order from payment In the default transaction process and default template flow, the order is initiated and processed in [CheckoutPageWithPayment.js](https://github.com/sharetribe/web-template/blob/main/src/containers/CheckoutPage/CheckoutPageWithPayment.js) using _processCheckoutWithPayment()_. [_ListingPage.shared.js_ passes initial values](https://github.com/sharetribe/web-template/blob/main/src/containers/ListingPage/ListingPage.shared.js) in its `handleSubmit()` with `callSetInitialValues()`, and those initial values get handled on the checkout page. Since the off-session payment process separates initiating the order (i.e. creating a booking, setting line items in a privileged transition) from payment (creating and further processing Stripe payment intent), it is important to pay attention to the way you want to handle that separation. - What happens when user clicks 'Request to book' on the listing page? - Where is the API call made to invoke the initial process transition that creates a booking and sets line items? ### Handling delayed manual payment If the automatic payment succeeds, the customer does not need to take further action on the transaction before the review process. Manual payment, on the other hand, does require a new user flow in the template. [CheckoutPage.js](https://github.com/sharetribe/web-template/blob/main/src/containers/CheckoutPage/CheckoutPage.js) is set up to handle payments toward Stripe, so the simplest option is that after an automatic payment has not succeeded and the customer has manually triggered the transition to create a payment intent, they are redirected to CheckoutPage.js (cf. [TransactionPage.js](https://github.com/sharetribe/web-template/blob/main/src/containers/TransactionPage/TransactionPage.js) `redirectToCheckoutPageWithInitialValues()`) to continue the process. Pay attention to the following points when designing your user flow: - What action does the customer take in the UI to initiate manual payment? - Does the provider see whether or not the customer has paid for the booking? --- ## How PaymentIntents work Path: concepts/payments/payment-intents/index.mdx # How PaymentIntents work with the Sharetribe Stripe integration ## Introduction [PaymentIntents](https://stripe.com/docs/payments/payment-intents) are a mechanism provided by Stripe to track the lifecycle of customer checkout flow. In addition, PaymentIntents provide tools for [Strong Customer Authentication (SCA)](/template/payments/strong-customer-authentication/) where required. Sharetribe has built-in support for PaymentIntents and Strong Customer Authentication. PaymentIntents provide fraud prevention with things like [3D Secure Card Payments](https://stripe.com/docs/payments/3d-secure), and allow a variety of payment methods to be used when making a payment in Sharetribe. See the [overview of supported payment methods in Sharetribe](/concepts/payments/payment-methods-overview/). This article describes how PaymentIntents relate to Sharetribe transaction process payment logic, as well as the general principles of implementing a checkout flow with PaymentIntents. ## Transaction process with PaymentIntents On high level, the payment flow with PaymentIntents has the following steps: 1. Customer initiates or transitions a transaction with a transition 2. Customer uses the PaymentIntent data to complete any steps necessary to authenticate and authorize the payment. 3. The transaction can proceed only after customer has authorized (if required) the payment. The PaymentIntent is confirmed, resulting in a Charge being preauthorized (in the case of [card payments](/concepts/payments/payment-methods-overview/#card-payment-flow)) or fully captured (in the case of [push payment methods](/concepts/payments/payment-methods-overview/#push-payment-flow)). 4. Transaction flow continues with the next appropriate transition. ### Example transaction process with card payments ![Automatic PaymentIntent flow](./automatic_confirmation_flow.png) For technical implementation of PaymentIntents, Stripe offers two approaches - [manual or automatic confirmation flow](https://stripe.com/docs/payments/payment-intents#one-time-payments). Sharetribe uses the automatic flow. In practice, the Sharetribe transaction engine models the automatic flow with two transitions. First transition creates the PaymentIntent (Step 1.) and second transition will validate and mark it confirmed in Sharetribe (Step 3.). Between these steps, the automatic flow pushes the responsibility of authenticating, authorizing and confirming the payment in Stripe to the client application (Step 2.). More information on the Step 2. can be found in this [section](#required-actions-in-the-client). ### Example transaction process with both card and push payments ![PaymentIntent process with card and push payments](./push-payment-process.png) Since push payments [do not have a preauthorization stage](/concepts/payments/payment-methods-overview/#push-payment-flow), this process allows an instant-booking type of flow, where the booking does not need acceptance from the provider. You can find another example process with only an _instant booking_ flow and support for both card and push payments in the [Instant booking process](https://github.com/sharetribe/example-processes#instant-booking) in the [Sharetribe example transaction processes repository](https://github.com/sharetribe/example-processes). ## Actions related to PaymentIntents The following actions can be attached to a transaction process in order to implement PaymentIntent flow and are already present in the default flows. ### stripe-create-payment-intent Creates a PaymentIntent for use with card payments (or payment methods that are similar, such as Google Pay or Apple Pay). You can optionally pass in a [PaymentMethod](https://stripe.com/docs/payments/payment-methods) ID, or attach a PaymentMethod later to the transaction during the validation and confirmation in the client by using Stripe Elements. The latter option is what we recommend you use. For detailed reference, see [here](/references/transaction-process-actions/#actionstripe-create-payment-intent). ### stripe-create-payment-intent-push Creates a PaymentIntent for use with push payments. You can optionally pass in a [PaymentMethod](https://stripe.com/docs/payments/payment-methods) ID, or attach a PaymentMethod later to the transaction during the validation and confirmation in the client by using Stripe Elements. The latter option is what we recommend you use. For detailed reference, see [here](/references/transaction-process-actions/#actionstripe-create-payment-intent-push). ### stripe-confirm-payment-intent Validates that the transaction has a PaymentIntent created and verifies via Stripe API that the PaymentIntent status is `requires_capture`, `requires_confirmation` or `succeeded` (only allowed for push payment methods). Confirms the PaymentIntent in Stripe, if needed. If the payment intent was created with `stripe-create-payment-intent` (a card payment), a preauthorization is placed on the card. The payment then can be captured in full by using `stripe-capture-payment-intent` within 7 days of creating the payment intent, or the preauthorization can be released by using `stripe-refund-payment`. On the other hand, if the payment intent was created with `stripe-create-payment-intent-push`, there is no preauthorization, the payment is captured in full and there is no need to use the `stripe-capture-payment-intent` action. The payment can be refunded in full using the `stripe-refund-payment`. For detailed reference, see [here](/references/transaction-process-actions/#actionstripe-confirm-payment-intent). ### stripe-capture-payment-intent Captures a confirmed PaymentIntent. In case of PaymentIntents created through `stripe-create-payment-intent-push`, the PaymentIntent is automatically captured already when confirmed and this action has no effect. Note that uncaptured payment intents are valid for seven days, after which they are automatically canceled by Stripe. For detailed reference, see [here](/references/transaction-process-actions/#actionstripe-capture-payment-intent). ### stripe-refund-payment Either cancels an unconfirmed PaymentIntent or refunds the related captured charge. For detailed reference, see [here](/references/transaction-process-actions/#actionstripe-refund-payment). ## Required actions in the client The required actions in the client are related to authentication and confirmation. You need to be able to handle potential authentication steps required by the customer's card issuing bank. After authentication, the client needs to obtain PaymentIntent data from the transaction's protected data ([as described in step 2 below](#step-2-collect-payment-information-and-handle-customer-actions)) and use that to confirm the payment. These steps are already implemented in the Sharetribe Web Template. In case you want to enforce [3D Secure Card Payments](https://stripe.com/docs/payments/3d-secure) for cards that support 3DS, in addition to supporting payment authentication in your client app, you may need to update your [Stripe Radar rules](https://stripe.com/docs/payments/3d-secure#three-ds-radar). ### Handling Strong Customer Authentication [Strong Customer Authentication](https://stripe.com/en-fi/payments/strong-customer-authentication) is a potential step enforced by governmental regulation. Not every PaymentIntent for card payments will require customer authentication. For instance, authentication may not be required for: - transactions out of scope of SCA - e.g. when card issuing bank is outside of EEA - merchant initiated transactions - transactions that fall under an SCA exemption - low value or low risk transactions - recurring payments for fixed amount - other In addition, PaymentIntents for push payment methods also require customer action. Typically, the customer needs to be redirected to their bank website or app where they can complete the payment, after which they get redirected back to the marketplace. This means that Sharetribe implementation of PaymentIntents supports payment flows that require authentication and those that do not. When implementing the PaymentIntent flow in the client you need to be prepared for handling both cases - payments requiring SCA and payments that do not. It might be impossible to know in advance whether the payment will require authentication, unless the marketplace and all its customers are outside of EEA. The recommended way of implementing support for SCA is to use [Stripe Elements](https://stripe.com/docs/payments/payment-intents/quickstart) that can provide you with ready modals for handling e.g. 3D Secure Card Payments. The next section will provide high level instructions on how to do this in the client. ### Implementing the PaymentIntent flow For implementing the PaymentIntent flow, you can use the following guides as a reference: - [card payments](https://stripe.com/docs/payments/accept-a-payment) - push payment methods: - [Alipay](https://stripe.com/docs/payments/alipay/accept-a-payment) - [Bancontact](https://stripe.com/docs/payments/bancontact/accept-a-payment) - [EPS](https://stripe.com/docs/payments/eps/accept-a-payment) - [giropay](https://stripe.com/docs/payments/giropay/accept-a-payment) - [iDEAL](https://stripe.com/docs/payments/ideal/accept-a-payment) - [Przelewy24](https://stripe.com/docs/payments/p24/accept-a-payment) Below we outline the concrete steps and how they work in combination with the Sharetribe transaction process. #### Initiate or transition a Sharetribe transaction With Sharetribe, the step to create a PaymentIntent in handled by the transaction engine when a transaction transitions with a transition using one of the following actions: - [stripe-create-payment-intent](#stripe-create-payment-intent) - use this action for card payments - [stripe-create-payment-intent-push](#stripe-create-payment-intent-push) - use this action for payments with push payment methods If we assume that your transaction process has two initiating transitions, as in the image below, you would use the `request-card-payment` transition for card payments and the `request-push-payment` transition for push payments. ![Transaction initial transitions for card and push payments](./request-transitions-regular-push.png) #### Collect payment information and handle customer actions [Stripe Elements](https://stripe.com/docs/stripe-js) provides ready tools and a reference for implementing the automatic PaymentIntent flow. It is useful for both collecting payment details, attaching the PaymentMethod to the PaymentIntent, as well as handling any customer payment authentication or confirmation steps. It's the recommended way to support PaymentIntents in the client. For card payments, your implementation will typically invoke a call to Stripe.js [stripe.confirmCardPayment](https://stripe.com/docs/js/payment_intents/confirm_card_payment). For push payments, the correct Stripe.js method depends on the concrete payment system. See [here](https://stripe.com/docs/js/payment_intents) for a full list of Stripe.js methods. In either case, you need the PaymentIntent's ID and client secret. Both of those values are exposed in the transaction's protectedData map under a key `stripePaymentIntents` after the PaymentIntent has been created. The value of `stripePaymentIntents` is an object in the form of: ```json { "default": { "stripePaymentIntentId": "pi_1EXSEzLSea1GQQ9x5PnNTeuS", "stripePaymentIntentClientSecret": "pi_1EXSEzLSea1GQQ9x5PnNTeuS_secret_Qau2uE5J5L6baPs8eLPMa2Swb" } } ``` This data is only exposed to the customer in the transaction. The provider can not access the PaymentIntent ID or the client secret. #### Transition the Sharetribe transaction further Once any customer authentication or payment confirmation is handled in the UI, you need to transition the Sharetribe transaction further in order for Sharetribe to record the payment details correctly. Make sure that the transition includes the [stripe-confirm-payment-intent](#stripe-confirm-payment-intent) action. If we assume that your transaction process uses both card and push payments as in the image below, you would use the `confirm-payment` transition for card payments and the `confirm-payment-instant-booking` transition for push payments. ![Confirm transitions for instant and card payments](./confirm-transitions-card-instant.png) ## Further reading - [Payment methods overview](/concepts/payments/payment-methods-overview/) * [Transaction process](/concepts/transactions/transaction-process/) * [Action reference for Stripe integration](/references/transaction-process-actions/#stripe-integration) * [Editing transaction process](/how-to/transaction-process/edit-transaction-process-with-sharetribe-cli/) * [Changing transaction process setup in Sharetribe Web Template](/how-to/transaction-process/change-transaction-process-in-template/) --- ## Payment methods overview Path: concepts/payments/payment-methods-overview/index.mdx # Payment methods overview ## Introduction to payments in Sharetribe Enabling customers to pay for their bookings and handle the transaction flow efficiently is one of the most valuable features of Sharetribe. Sharetribe leverages Stripe for payments and offers multiple different ways for customers to pay for transactions. The following are the currently supported payment methods, divided into two categories - pull and push payment methods: - Pull payment methods - card payments - similar to card payments (Google Pay, Apple Pay, Microsoft Pay) - Push payment methods - Alipay - Bancontact - EPS - giropay - iDEAL - Przelewy24 Stripe offers PayPal as a payment method. However, it is not supported in the Sharetribe Stripe integration, because it does not support the `on_behalf_of` parameter for destination charges [(see Stripe Docs)](https://docs.stripe.com/payments/paypal#connect). In Stripe's terms, Sharetribe supports payment methods that have _immediate payment confirmation_ (see the table [in this section of Stripe's guide to payments](https://stripe.com/en-fi/payments/payment-methods-guide#2-choosing-the-right-payment-methods-for-your-business)) and that are supported by Stripe's PaymentIntents API. Mostly, these payment methods fall under either Cards, [Bank redirects](https://stripe.com/docs/payments/payment-methods/overview#bank-redirects), or [Wallets](https://stripe.com/docs/payments/payment-methods/overview#wallets) in Stripe's classification. Sharetribe does not support payment methods that require the use of Stripe's older Sources API. This article presents how payments flow depending on whether you use card (or similar) payments or have enabled any other push payment method. The article also describes the tradeoffs you need to consider in designing the transaction process involving payments. ## Money flow in different scenarios Understanding how money flows when using card payments or push payments is crucial for understanding how to design the transaction process in Sharetribe. ### Card payment flow For card payments, the payment flow starts with a preauthorization. The money is reserved but not yet charged from the customer's credit card. After a charge, Stripe moves money to the provider's Connect Account Balance in Stripe and holds it there until a payout is issued. ![Card payment flow](./card-payment-flow.png 'Card payment flow in Sharetribe.') The preauthorization is valid for seven days, after which Stripe automatically releases it. You can use the preauthorization to configure the transaction process to allow the provider to accept or decline the transaction. If the provider declines the transaction, Stripe collects no processing fees since money has not been transferred. In the default purchase and negotiation processes, a charge is made immediately after payment, so there is no preauthorization period. This means that refunds can be issued only at a point where money has already moved and the marketplace must cover Stripe fees. The transaction process can hold money in Stripe until an explicit payout (the hold should not exceed 90 days). The mechanism allows you to build a transaction process where the money is in an escrow-like hold until an explicit payout. The hold ensures some safety for the customer as the marketplace controls the money until a payout. The process can issue the payout in a separate transition. ### Push payment flow For push payments, there is no preauthorization. Once the customer confirms the payment, it gets captured automatically and a charge is made immediately. The charge moves money immediately to the provider's Stripe Connect account. ![Push payment flow](./push-payment-flow.png 'Push payment flow in Sharetribe.') This means that refunds can be issued only at a point where money has already moved and the marketplace must cover Stripe fees. You should take this into account when designing the transaction process of your marketplace. Either declining the transaction should be disabled, handled through availability management, or accounted for in commissions. The rest of the money flow is the same as with card payments and has the same features and capabilities. ## Using different payment methods in your marketplace ### Customizing the transaction process The [default processes](https://github.com/sharetribe/example-processes/tree/master/) in Sharetribe supports card payments. The general article on [the transaction process](/concepts/transactions/transaction-process/) describes the process in more detail. Even though Google Pay, Apple Pay, and Microsoft Pay are similar to card payments, they require some changes to the default implementation of Sharetribe Web Template. To enable them in the template, you need to follow the [Stripe instructions on the Request Payment Button](https://stripe.com/docs/stripe-js/elements/payment-request-button). If you wish to enable push payments, you need to adapt your transaction process. For instance, you need to add a new transition that includes the [stripe-create-payment-intent-push](/references/transaction-process-actions/#actionstripe-create-payment-intent-push) action. Further, because push payments do not have a preauthorization phase, it is recommended to avoid that in the transaction process and use an _instant booking_ type of flow. The example below describes the minimum recommended changes in the two transitions: `request-push-payment` and `confirm-payment-instant-booking`. The example illustrates how you can still use the preauthorization step for card payments. ![Push payment process](./push-payment-process.png 'Push payment process example.') You can find another example process with only an _instant booking_ flow and support for both card and push payments in the [Instant booking process](https://github.com/sharetribe/example-processes#instant-booking) in the [Sharetribe example transaction processes repository](https://github.com/sharetribe/example-processes). ### Handling payment methods in the client app See [this section in the PaymentIntents guide](/concepts/payments/payment-intents/#required-actions-in-the-client). ## Further reading For further reading on the subject, see the following articles that describe how to edit the transaction process: - [How PaymentIntents work](/concepts/payments/payment-intents/) - [Transaction process](/concepts/transactions/transaction-process/) - [Action reference for Stripe integration](/references/transaction-process-actions/#stripe-integration) - [Editing transaction process](/how-to/transaction-process/edit-transaction-process-with-sharetribe-cli/) - [Changing transaction process setup in Sharetribe Web Template](/how-to/transaction-process/change-transaction-process-in-template/) --- ## Payments in Sharetribe Path: concepts/payments/payments-overview/index.mdx # Payments Payments in Sharetribe are powered by Stripe, using [Stripe Connect](https://stripe.com/en-fi/connect/) with [Custom Accounts](https://stripe.com/docs/connect/custom-accounts). ![Payment gateway logic](./payment-flow.png) ## Payments with Stripe All default transaction processes are built to support Stripe. The Sharetribe Web Template also comes with built-in support for payments with Stripe. The built-in Stripe integration requires you to use: - [Stripe Connect with Custom accounts](https://stripe.com/docs/connect/custom-accounts) - [Destination charges](https://stripe.com/docs/connect/destination-charges) with the `on_behalf_of` parameter Commissions are collected as [application fees](https://stripe.com/docs/connect/destination-charges#application-fee). Once the transaction is successfully over, the provider's share is paid out to the bank account that the provider gave upon onboarding. The customer can check out using a payment card or [another supported method](#payment-methods-and-currencies), and they can also save their payment method for future use.
"What does "destination charge" mean?" The Stripe `on_behalf_of` destination charge means that when the charge is created, the money goes directly to the provider's Custom Connect account in Stripe, and the provider's information is shown on the customer's payment method receipt. This also means that a listing cannot be booked or purchased if the provider has not onboarded to Stripe, because the charge cannot be created in Stripe without the provider's Custom Connect account information.
### Default payment process with Stripe ![Default payment flow in Sharetribe](./automatic_confirmation_flow.png) #### Provider onboarding In the Sharetribe default integration, users need to have a Stripe account with a bank account set up before others can initiate transactions with them successfully. This is done by [creating a Stripe account](https://www.sharetribe.com/api-reference/marketplace.html#create-stripe-account) for the authenticated user. The [Sharetribe Web Template](/concepts/payments/providers-and-customers-on-stripe-platform/) is configured to do this step out-of-the-box using [Stripe Connect Onboarding](https://stripe.com/en-fi/connect/onboarding). To create the account, Stripe requires verification information from the provider, and the specific types of verification information depend on the provider's country. You can check the verification requirements for your most likely marketplace provider demographics in [Stripe's own documentation](https://stripe.com/docs/connect/required-verification-information). #### Customer checkout When the customer triggers a payment related transition in the Sharetribe default transaction processes, Sharetribe creates a [PaymentIntent](/concepts/payment-intents/) for the total price of the transaction. Once the PaymentIntent is confirmed, Stripe preauthorizes the sum from the customer's payment method. In other words, even though the sum is not paid out from the customer's card, it is reserved and not available to be used by the customer. The preauthorization is valid for 7 days, after which the preauthorization is automatically released by Stripe, and the funds are again available to the customer. Creating and confirming the PaymentIntent happen in the same transition in both the default purchase process and the default negotiation process. **Related Stripe actions:** - [:action/stripe-create-payment-intent](/references/transaction-process-actions/#actionstripe-create-payment-intent) - [:action/stripe-confirm-payment-intent](/references/transaction-process-actions/#actionstripe-confirm-payment-intent) #### Provider acceptance The default purchase and negotiation processes in the Sharetribe Web Template uses an instant checkout, which means customer checkout and purchase acceptance are all triggered on the same customer action. In other words, provider acceptance is not necessary. However, it is possible to add the provider acceptance step to these processes as well. In the default booking process, on the other hand, a provider has 6 days to accept the booking until it expires automatically. This timeline ensures that the Stripe preauthorization does not expire before the provider has the opportunity to accept or reject the booking. Once the provider accepts the booking, the PaymentIntent is captured and the transaction sum is transferred from the customer's card to the provider's Custom Connect account. If the transaction has any commissions, those are then paid from the provider's Connect account to the platform's account as an [application fee](https://stripe.com/docs/api/application_fees). Depending on how the [transaction's line items](/concepts/pricing-and-commissions/pricing/#line-items) have been defined, the platform can take a [commission of the price](/concepts/pricing-and-commissions/commissions-and-monetizing-your-platform/) from either the provider, the customer, or both. The platform is also responsible for paying all [Stripe fees](https://stripe.com/en-fi/connect/pricing) related to the Custom Connect account usage, so the commissions must be defined to cover those expenses as well. **Related Stripe actions:** - [:action/stripe-capture-payment-intent](/references/transaction-process-actions/#actionstripe-capture-payment-intent) #### Customer refund If the customer requests a refund for one reason or another, the operator can refund the PaymentIntent. The Sharetribe integration with Stripe only supports full refunds. (Handling partial refunds is discussed [later in this article](/concepts/payments/payments-overview/#how-can-i-partially-refund-transactions-in-my-sharetribe-marketplace).) The default transaction process takes into account whether or not the PaymentIntent has already been captured from the customer's account. **Related Stripe actions:** - [:action/stripe-refund-payment](/references/transaction-process-actions/#actionstripe-refund-payment) #### Provider payout Once the order has completed successfully, the provider's payout is paid to the bank account that is linked to their Custom Connect account. It is important to note that Stripe can [hold funds for up to 90 days (with some exceptions)](https://stripe.com/docs/connect/account-balances#holding-funds). In other words, the payout must be triggered no more than 90 days after the PaymentIntent is created. This means that for booking or purchase times exceeding 90 days, the process needs to be modified. **Related Stripe actions:** - [:action/stripe-create-payout](/references/transaction-process-actions/#actionstripe-create-payout)
Manual or automatic payout? In Stripe terms, the Sharetribe integration uses manual payouts. This means that Stripe does not automatically pay out the funds from the Connect accounts e.g. daily or weekly, and instead the platform controls the payout schedule. Since the payouts are triggered by the transaction process, they happen automatically from the marketplace operator's point of view. In other words, the operator should not pay out funds manually through the Stripe Dashboard if the marketplace transaction process is using the Stripe payout action.
### Modifications to the default processes One of the strengths of Sharetribe is that you have complete control over the transaction process. In terms of payments, you can make parallel paths depending on your payment strategy, and you can fine-tune the timeline of different actions to suit your marketplace. Even when using parallel paths, a single transaction can only ever have a single PaymentIntent associated with it. When customizing your transaction process, make sure that a single path can only process a single payment to avoid issues. You can edit the transaction processes on your marketplace with [Sharetribe CLI](/how-to/transaction-process/edit-transaction-process-with-sharetribe-cli/). If you use Sharetribe Web Template, you will also need to make some [changes in the template](/how-to/transaction-process/change-transaction-process-in-template/) to enable it to use a different process. If you do make changes to a transaction process when you already have transactions in your environment, it is good to note that a transaction will proceed with the transaction process it was initiated with, and changing the transaction process of a single transaction is not possible. You can see the transaction process related to each transaction in [Sharetribe Console](https://console.sharetribe.com/) > Manage > Transactions. The transaction process also controls the automatic [email notifications](/references/email-templates/) sent at different stages of the transaction flow. When you make changes to the transaction process, be sure to also update the wording and logic of the notifications for a consistent user experience for your marketplace customers and providers. #### Instant booking As mentioned, both the default purchase process and the default negotiation process combine the customer checkout and provider acceptance steps into a single customer action. In other words, the order is automatically accepted and paid as soon as the customer clicks to pay for the transaction. The [example-processes Github repository](https://github.com/sharetribe/example-processes) contains an example of a booking process called `instant-booking` that you can use to implement a similar flow for bookings in Sharetribe Web Template, as well as in any custom client application you may be using. #### Automatic off-session payments Another notable approach to modifying the payment timeline in Sharetribe is the [off-session payment pattern](/concepts/payments/off-session-payments-in-transaction-process/). In an off-session payment, the customer checkout and provider acceptance happen when the customer books or purchases the listing, but the payment takes place at a later date. That way, customers can, for instance, book listings or purchase preorder products further in the future than the 90 day Stripe limitation, and they will be charged closer to the moment of receiving the product or service they purchased. ### Negotiated payments The [negotiation transaction process](/concepts/transactions/negotiation-process/) is one of the default Sharetribe transaction processes, and it involves a negotiated payment. In this process, the listing price does not determine the price of the transaction, and instead the customer and provider negotiate the price between them. Once both the provider and customer have accepted an offer, the customer proceeds to make a payment based on the quote. ### Payment methods and currencies Sharetribe supports multiple payment methods as a part of its Stripe integration. The default payment method is a payment card, which is what the Sharetribe Web Template uses. However, you can enable [other payment methods](/concepts/payments/payment-methods-overview/) as well with moderate custom development work. The user can save a default payment method in Sharetribe. If your marketplace uses the [automatic off-session payment flow](#automatic-off-session-payments), the customer must save their payment method so that the transaction process can try to automatically charge them at the specified moment. Sharetribe does not determine a currency for listings. However, each listing needs to have a currency specified in its `price` attribute. The value for `price.amount` is given in the minor unit of `price.currency` (e.g. cents for USD). Sharetribe Console displays listing prices based on the listing's currency. The Sharetribe Web Template has a single currency defined by default, to facilitate e.g. price filtering and sorting. As the transaction progresses, the payment intent is created and charged from the customer's payment method in the listing's currency, or alternatively the [currency of the line items](/concepts/pricing-and-commissions/pricing/#line-items) if different from the listing currency. The payout currency is determined by the provider's bank account currency.
Stripe currency terminology In Stripe terminology, the **presentment currency** is the currency of the charge, i.e. the currency at which the listing price is charged from the customer's card. The customer's card provider may charge a conversion fee if the presentment currency differs from the customer's card currency, or if the credit card and the marketplace platform are registered in different countries regardless of currency. The **settlement currency** is the currency of the payout, i.e. the currency at which the provider's payout is paid to their bank account. If the presentment currency differs from the settlement currency, Stripe converts the charge to the settlement currency. See [Stripe's own documentation](https://stripe.com/docs/currencies) for country-specific details on supported currencies.
## Sharetribe Web Template and Stripe The Sharetribe Web Template comes with a working Stripe integration. - [Provider onboarding](/concepts/payments/providers-and-customers-on-stripe-platform/) is handled with Stripe Connect Onboarding. A provider cannot create listings (i.e. receive money from customers) unless they have verified their identity with Stripe — this ensures that the platform is always KYC compliant. - _CheckoutPage.js_ and its subcomponent _CheckoutPageWithPayment.js_ handle Stripe actions related to customer checkout in default processes. Creating and confirming the payment intent are handled with a single button click. - The customer can save their payment method to Sharetribe either when making a payment on a transaction, or on a separate Payment Methods page. ## Frequently asked questions about payments ### Is Stripe supported in my country? In order to use Stripe for your marketplace, your platform account needs to be in a Stripe-supported country. Refer to our [Help Center](https://www.sharetribe.com/help/en/articles/8418388-countries-and-currencies-supported-by-stripe) for more information. #### Can I add more Stripe countries to the Sharetribe Web Template? [#add-countries] The Sharetribe Web Template by default includes all countries that support the Sharetribe Stripe integration. In addition to being Stripe-supported in general, the country needs to support the following Stripe features: - Custom account types - Destination charges with the `on_behalf_of` API parameter - Application fees - Manual payout setting - 90-day holding period We do regularly check whether support for new countries is added for Stripe Connect with Custom accounts and these specified requirements, and we add new supported countries when we become aware of them. If you notice that your country supports all of these attributes and is not included in the Stripe supported countries in Sharetribe Web Template, do reach out to us! #### Known exceptions related to Brazil, India, and Hungary Stripe support [#country-exceptions] The Sharetribe Web Template does not support Brazil (BR), India (IN) and Hungary (HU), even though all three countries are mentioned as available Stripe countries in [Stripe's documentation](https://stripe.com/docs/connect/accounts#custom-accounts). If you want to support one of these three regions, you will need to do a fair amount of customization on top of the default Sharetribe setup. - The Sharetribe transaction engine uses manual payouts, which are{' '} [not supported for Brazil and India](https://stripe.com/docs/payouts#manual-payouts). - India has restrictions on [cross-border payments](https://support.stripe.com/questions/stripe-india-support-for-marketplaces). - Stripe treats the Hungarian currency HUF as a [zero-decimal currency for payouts](https://stripe.com/docs/currencies#special-cases). This means that even though the Sharetribe engine can create charges in two-decimal amounts (e.g. HUF 20.38), payouts can only be created in integer amounts evenly divisible by 100 (e.g. HUF 20.00). Additionally, if Stripe needs to do currency conversions from another currency to HUF, the resulting amount may have decimals which can cause the payout to fail. ### I'm having problems with the Stripe integration [#stripe-integration-problems] Sometimes it takes a while to get Stripe to work. Here are some ideas to troubleshoot the problem. - Double check that you have followed the [Stripe setup instructions](https://www.sharetribe.com/help/en/articles/8413086-how-to-set-up-stripe-for-payments-on-your-marketplace). Note that in your Dev and Test environments, you need to use the Stripe keys starting with `sk_test` and `pk_test`, and you will also need to use [Stripe's test payout details](https://stripe.com/docs/connect/testing#payouts) and [test payment methods](https://stripe.com/docs/testing#payment-intents-api) with those test keys. In the Live environment with real payment methods, you will need to use the keys starting with `sk_live` and `pk_live`. Also check that the keys you are using match the keys in Stripe Dashboard. You can "roll", i.e. refresh the keys if necessary and enter the new keys — they will still be connected to the same Stripe platform account. - If you get your Stripe integration working to the point that you get an error message from Stripe, it is useful to take a moment to check [what the error code means](https://stripe.com/docs/error-codes). It is also often useful to put the error code into a search engine and check if someone has already solved a similar problem. - In case of payout problem issues, you can check out our article about [Stripe payout issues](https://www.sharetribe.com/help/en/articles/10006947) for advice or ideas. If nothing seems to work, you can always contact Sharetribe technical support through the chat widget in the [Sharetribe Console](https://console.sharetribe.com/) or [by email](mailto:hello@sharetribe.com) for further troubleshooting. ### How to implement partial refunds? The default Stripe integration in Sharetribe only supports full refunds. If you have a use case where you would need to implement partial refunds, here are some options you can consider. All of these require some degree of custom development effort. #### Multiple transactions for one purchase If you want to keep using the default Sharetribe Stripe integration, you can look into triggering two transactions in Sharetribe for a single purchase — one for the main price, and one for the refundable part of the price. Each transaction would have its own PaymentIntent towards Stripe, so you would need to implement separate transaction processes for each type of transaction to handle the PaymentIntents, and the payments would show up as two different charges on the customer's account. Furthermore, you would need to coordinate commission amounts, as well as the possibility of a full refund, e.g. if the booking is cancelled by the provider for one reason or another. Also bear in mind that if you trigger two transactions for the same payment method in quick succession, some card providers may flag this as suspect behavior, so you will need to consider the timing of the transactions carefully. #### Partial third party payment integration In this option, you would use the Sharetribe default payment integration up to the point where the payment gets captured onto the provider's Custom Connect account. You would then handle all payouts and refunds manually, i.e. outside the Sharetribe transaction process — either in Stripe Dashboard, or through the Stripe API with your own integration. This would require you to keep track of the correct sums to be paid out for each transaction yourself. This option poses the risk of causing payout issues for completely unrelated transactions; Stripe does not separate funds by PaymentIntent, so a miscalculated excessive refund on a transaction between provider A and customer B may cause payout to fail for customer C on a different transaction. You can read more on [payout issues on manual refunds](https://www.sharetribe.com/help/en/articles/10006947) to figure out what you would need to consider to implement this option successfully. #### Full third party payment integration Of course, you can create a fully separate third party payment integration to handle creating and capturing the payments as well as managing payouts and refunds. This gives you the greatest flexibility with your setup, and conversely it requires more customization and development. You can refer to our high-level instructions on [integrating a 3rd-party payment gateway](/how-to/payments/how-to-integrate-3rd-party-payment-gateway/) to find out whether this option would best suit your needs. If you are contemplating partial refunds for your marketplace, you can also contact Sharetribe technical support through the chat widget in your [Sharetribe Console](https://console.sharetribe.com/) or [by email](mailto:hello@sharetribe.com). Let us know your specific use case, and we may be able to recommend some avenues for you to explore. ### Can I use Sharetribe and not use Stripe? You can absolutely use Sharetribe without using Stripe. You might not use payments at all in your marketplace, or your platform operates in a non–Stripe supported country, or you may have some other reason. Using the Sharetribe backend without the Stripe integration is fairly simple. You will need to remove references to all [Stripe-related transaction process actions](/references/transaction-process-actions/#stripe-integration) from your transaction processes, and avoid using [Stripe-related endpoints](https://www.sharetribe.com/api-reference/marketplace.html). For clarity, all references to Stripe's backend elements (endpoints, transaction process actions etc.) are named with the prefix `stripe`. If you want to modify your template to work without Stripe, the effort is more extensive, since each template is built around a logic that uses Stripe actions and endpoints. You can use [this article](/how-to/payments/removing-stripe-and-payments/) as your starting point. When removing the Stripe integration, you will want to consider whether or not you want to implement some other payment gateway in your marketplace to handle payments. You can refer to our high-level instructions on [how to integrate a 3rd-party payment gateway](/how-to/payments/how-to-integrate-3rd-party-payment-gateway/) when making the decision and when implementing any changes. --- ## Providers and customers on your Stripe Platform Path: concepts/payments/providers-and-customers-on-stripe-platform/index.mdx # Providers and customers on your Stripe Platform ## Introduction If your marketplace handles payments through the Sharetribe Stripe integration, your providers and customers will show up in different ways on your Stripe platform account. The main ways this happens is through Stripe Connect accounts and Stripe Customers. The Sharetribe Stripe integration uses Stripe Custom Connect accounts for providers. A provider needs to onboard to Stripe Connect before they can receive payouts on their own listings or submit offers on customer listings. For customers, on the other hand, a Stripe Customer does not need to be created by default. ## Stripe for providers: Stripe Connect Onboarding When your marketplace handles payments, it is important to have a provider verification process where providers can enter all necessary and required information for them to receive payments. Provider verification is [a requirement from Stripe's side](https://support.stripe.com/questions/know-your-customer-obligations) for identity verification, risk assessment, and avoiding money laundering and other types of financial crime. Regulatory aspects of provider onboarding can be challenging and changing rapidly. Stripe Connect Onboarding provides ready tools for meeting the requirements and reducing the operational complexity of self-managing the onboarding flow and identity verification. For both Sharetribe Web Template and custom implementations, your marketplace will first need to set up Stripe for payments and enable Connect Onboarding. You can review the [instructions in our Help Center](https://www.sharetribe.com/help/en/articles/8413086-how-to-set-up-stripe-for-payments-on-your-marketplace) for more details. ### Stripe Connect Custom Accounts Sharetribe uses Stripe Connect with Custom Accounts as the default payment integration. These enable your sellers to process payments through your marketplace. Stripe Connect also has other types of accounts, such as Standard and Express, but those are not compatible with the Sharetribe Stripe integration. When you use the Sharetribe Stripe integration, you need to create Custom Connect accounts for your users through the Sharetribe Marketplace API [Stripe Account creation endpoint](https://www.sharetribe.com/api-reference/marketplace.html#create-stripe-account). It is not possible to associate an existing Custom Connect account with a Sharetribe user profile. When you need to update the Stripe Account, you can either use the Marketplace API [Stripe Account updating endpoint](https://www.sharetribe.com/api-reference/marketplace.html#update-stripe-account) or directly through [Stripe API](https://docs.stripe.com/api/accounts/update). ### Fetching an existing Stripe Connect Account If the provider already has a Stripe account associated with their Sharetribe account, the [currentUser resource](https://www.sharetribe.com/api-reference/marketplace.html#currentuser-resource-format) has a _stripeConnected_ boolean flag set to _true._ To fetch the provider's existing Stripe Connect account details, we can use the Marketplace API [Stripe Account fetch](https://www.sharetribe.com/api-reference/marketplace.html#fetch-stripe-account) endpoint. This will allow you to alert the provider if there is some required information missing from their Stripe Account. Stripe account data is returned after each create and update Stripe Account API call, so there is no need for separate fetch API call in these cases. After the Stripe Account has been fetched, you need to check _requirements_ in the _stripeAccountData_ attribute, which contains the related [Stripe Account Object](https://stripe.com/docs/api/accounts/object). If there are any fields in _past_due_ or _currently_due_ it means that those fields need to be collected to keep the account enabled. In other words, there are requirements missing. If there are no fields in _past_due_ or _currently_due_, it means that the verification is completed for now. It is still possible that there might be new fields to be collected if the account reaches the next volume thresholds. ### Creating a new Stripe Connect Account If the current user's _stripeConnected_ flag is false, the current user does not have a Stripe account, and you need to create a new Stripe Connect account for the user. You will need to create the Stripe Connect account through the Marketplace API, and it is not possible to associate an existing Stripe Connect account with a Sharetribe user account. If you collect bank account information in the client, [create a Stripe bank account token](https://docs.stripe.com/api/tokens/create_bank_account) so that you can securely pass it to the Sharetribe backend. If you collect other information in the client, such as account type (individual vs business) or whether the user has accepted Stripe's Connected Account Agreement, [create a Stripe account token](https://docs.stripe.com/api/tokens/create_account) to pass that information to the Sharetribe backend. With the Sharetribe Marketplace API, [create a Stripe account](https://www.sharetribe.com/api-reference/marketplace.html#create-stripe-account) and pass any tokens you created in the previous steps to the endpoint. The only mandatory parameter to create a Stripe Connect account through the Marketplace API is the country of the account, and it cannot be modified after the account has been created. In addition to the country information, we recommend passing the _requestedCapabilities_ parameter – the required capabilities for payments to work in Sharetribe are _card_payments_ and _transfers_, so you will need to define these for the account for it to work with the Sharetribe Stripe integration. There are also other optional parameters that you can collect in your application and pass to the endpoint. If you don't pass these details, the necessary information will be collected in the Stripe Connect Onboarding flow. Currently, Stripe doesn't support updating the country of the account after the account has been created. The account data is returned after each create and update Stripe Account API call, so there is no need for separate fetch API call in these cases. ### Handling verification status – Stripe Account Links After the Connect account exists, the user needs to complete the onboarding. The user can also update the information related to their Stripe Connect account by refilling the Stripe Connect Onboarding flow. This might be necessary if there are new requirements in the user's existing account, or if they want to for example update their contact information. Stripe Account Links are a mechanism for enabling your providers to access Stripe Connect Onboarding UI. You need to [create an account link](https://www.sharetribe.com/api-reference/marketplace.html#create-stripe-account-link) and provide the return URLs for success and failure cases. The response value is a URL where you can then redirect the provider to complete their Stripe Connect Onboarding. Do note that the Account Link has an expiration time, so you should fetch the link only when the user indicates they want to fill out the details. When creating the account link, you need to specify the type of the account link. You can also pass an optional _collectionOptions_ parameter to define which requirements you want to collect at that point. You can determine that you want your providers to fill out - only the information that is currently required for this account (_fields: currently_due_) - or also information that might become required for this account later if it reaches certain thresholds (_fields: eventually_due_) - and optionally information that is not yet required for any accounts, but will become required in the future (_future_requirements: include_) See the available values for these options in Stripe's own documentation: - [Account link types](https://docs.stripe.com/api/account_links/create#create_account_link-type) - [Account link _collectionOptions_](https://docs.stripe.com/api/account_links/create#create_account_link-collection_options) If the user returns to the success URL, it is still a good idea to check the status of the Stripe Account again. Returning to success URL does not automatically mean that the account has all the required information. ### Stripe Connect Account and Onboarding in Sharetribe Web Template The Sharetribe Web Template uses the Stripe Connect Onboarding flow by default. You can find the practical details in our help center: - [How adding payout details works with Stripe](https://www.sharetribe.com/help/en/articles/8857191-how-adding-payout-details-works-with-stripe) On the code level, the Stripe Connect Account creation and Connect Onboarding is mainly handled in the following files: - [StripePayoutPage](https://github.com/sharetribe/web-template/tree/main/src/containers/StripePayoutPage) - [StripeConnectAccountForm](https://github.com/sharetribe/web-template/tree/main/src/components/StripeConnectAccountForm) - [stripeConnectAccount.duck.js](https://github.com/sharetribe/web-template/blob/main/src/ducks/stripeConnectAccount.duck.js) In the `EditListingWizard` component, the modal with `StripeConnectAccountForm` is shown if the user doesn't have a Stripe Account yet or if there is some information missing from the account. The modal will be shown only if the user is publishing the listing. This means that users can update already published listing even if their Stripe Account is in the restricted state but they can't publish new listings. ### Using custom flow for Stripe provider onboarding It's also possible to implement the onboarding flow in your own application, if using Stripe Connect Onboarding is not an option. This way the user will stay in your application throughout the whole onboarding. The downside with this approach is that you are responsible for collecting all the required information and keeping the UI up-to-date also with the possible future changes. In general, we strongly recommend that you always use Stripe Connect Onboarding to onboard your providers, regardless of your front-end application. In our older [legacy templates](/legacy/how-to/provider-onboarding-and-identity-verification/#using-deprecated-payoutdetailsform-and-payoutdetailspage-as-a-starting-point), Stripe onboarding was implemented with a custom flow. There are some now deprecated components you can use as a starting point if you want to implement your own flow. You should keep in mind that these components will not be updated by our team since Sharetribe Web Template uses Connect Onboarding by default. You can find the deprecated files still from v.3.7.0 - [PayoutDetailsForm](https://github.com/sharetribe/ftw-daily/tree/v3.7.0/src/forms/PayoutDetailsForm) - [PayoutPreferencesPage](https://github.com/sharetribe/ftw-daily/tree/v3.7.0/src/containers/PayoutPreferencesPage) - [stripe.duck.js](https://github.com/sharetribe/ftw-daily/blob/v3.7.0/src/ducks/stripe.duck.js). ## Stripe for customers: saved payment methods Providers always need a Stripe Connect account to receive a payout from a transaction. For a customer, you can make a payment either with a Stripe Customer or without one. When you make a payment without a Stripe Customer, you create the PaymentIntent in the Sharetribe backend transaction process without passing payment method information to the transition. In that situation, you then need to attach the payment method to the PaymentIntent by making an API call to Stripe directly. If you want to create a Stripe Customer and save the payment card as you make the payment, you need to create the payment intent with a _setupPaymentMethodForSaving: true_ parameter. This sets up the PaymentIntent so that its PaymentMethod can later be attached to a Stripe Customer. In this flow, you 1. Create the PaymentIntent as a part of the transaction process 2. Pass the card information to Stripe API when you confirm the PaymentIntent 3. Use the Sharetribe API to [create the Stripe Customer](https://www.sharetribe.com/api-reference/marketplace.html#create-stripe-customer) and pass the PaymentIntent's _paymentMethodId_ as a parameter. You can also create a Stripe Customer without handling a payment at the same time. In that case, you need to create a [Stripe Setup Intent](https://www.sharetribe.com/api-reference/marketplace.html#create-stripe-setup-intents) with the Sharetribe API, and then use the setup intent to call Stripe handleCardSetup. Read more: - [Using stored payment cards](/concepts/payments/using-stored-payment-cards/) - [How saving a payment card works in the Sharetribe Web Template](/template/payments/save-payment-card/) ## Changing a marketplace's Stripe platform account Marketplaces sometimes need to change their Stripe platform account for one reason or another. Since Stripe Connect accounts and Stripe Customers are both associated with your Stripe platform account, it is not possible to change your Stripe keys if Connect accounts or Customers exist on your marketplace. You can reach out to Sharetribe support, and we can help you clear the Stripe Connect accounts and Customers from your marketplace. In practice, removing all Stripe Connect accounts and Stripe Customers means that all payout details (bank accounts) are lost from the marketplace users, and all providers will need to complete Stripe onboarding again before other users can start transactions against their listings. In addition, existing transactions that have an upcoming Stripe related action are not able to move forward. This means that if you need to change the Stripe keys for your Live marketplace, you will need to either cancel in Console all ongoing transactions that have upcoming Stripe actions, and then manage the necessary payouts and refunds manually on your old Stripe platform for transactions that cannot be canceled. --- ## Using stored payment cards Path: concepts/payments/using-stored-payment-cards/index.mdx # Using stored payment cards Sharetribe allows you to store the payment card of a customer for future purchases. Doing this provides multiple benefits: it streamlines the checkout process for existing customers and allows you to place additional charges to the payment card of the customer. ## Overview The Sharetribe Web Template provides an interface via which users can store their payment card for future use. Once they've stored it, they are offered the option to use the same card for subsequent purchases without entering the details again. Users can access their saved payment methods through their account settings. On this page, the user can store a new credit card, delete a stored card, or replace a stored card with a new one. Currently, it is only possible to save one card, which becomes the default payment method. I.e. if there is already one payment card saved, your only option is to replace the card with a new one. [Read API documentation on storing credit cards](https://www.sharetribe.com/api-reference/marketplace.html#stripe-customer) ## Saving a payment method outside a transaction You can save a payment method to a user profile without performing a payment at the same time. The user in question needs to be authenticated to perform these steps. ### Create Stripe Setup Intent Create Stripe [Setup Intent](https://www.sharetribe.com/api-reference/marketplace.html#stripe-setup-intents) through Sharetribe API. A Stripe Setup Intent enables you to save a payment method for future payments. Make a note of the client secret in the API response to use it in the next step. Read more about Stripe Setup Intents in Stripe's documentation: - [Setup Intents](https://docs.stripe.com/api/setup_intents) ### Collect payment method information in your user interface Your payment methods page needs to have a Stripe card element or payment element to collect the payment method's details. Currently, only cards are supported in Sharetribe as saved payment methods. ### Handle card setup with Stripe Call `stripe.confirmCardSetup` with the client secret from step 1. This endpoint will confirm the card setup, and handle user actions like 3D Secure authentication if required. Read more about `stripe.confirmCardSetup` in Stripe's documentation: - [stripe.confirmCardSetup](https://docs.stripe.com/js/setup_intents/confirm_card_setup) Sharetribe Web Template uses `stripe.handleCardSetup` to set up a payment method, and the parameters are different from `stripe.confirmCardSetup`. Read more about `stripe.handleCardSetup` in Stripe's documentation: - [stripe.handleCardSetup](https://docs.stripe.com/js/deprecated/handle_card_setup_element) ### Save the payment method to a Stripe Customer The response from `stripe.confirmCardSetup` (or `stripe.handleCardSetup`, if you are using the Sharetribe Web Template) contains the ID of the payment method created with the SetupIntent under `SetupIntent.payment_method`. To save that payment method to the current user, the user needs to have a Stripe Customer associated with their profile. First, fetch the `currentUser` resource and include the `stripeCustomer.defaultPaymentMethod` relationships to see if the user already has either an associated Stripe Customer or a saved default payment method. - If the user does not have a stripeCustomer attached, use the Sharetribe API `stripeCustomer.create` endpoint and pass the payment method id as the `stripePaymentMethodId` parameter. - If the user already has a `stripeCustomer`, but they don't have a `defaultPaymentMethod`, you can use `stripeCustomer.addPaymentMethod` to associate the payment method id with the user. - If the user has a `stripeCustomer.defaultPaymentMethod`, and you want to replace it with the new payment method, you need to first remove the previous card and then add the new one.
Endpoint(s) **No Stripe Customer** [stripeCustomer.create](https://www.sharetribe.com/api-reference/marketplace.html#create-stripe-customer) **Existing Stripe Customer, no `defaultPaymentMethod`** [stripeCustomer.addPaymentMethod](https://www.sharetribe.com/api-reference/marketplace.html#add-payment-method) **Existing Stripe Customer and `defaultPaymentMethod`** [stripeCustomer.deletePaymentMethod](https://www.sharetribe.com/api-reference/marketplace.html#delete-payment-method), [stripeCustomer.addPaymentMethod](https://www.sharetribe.com/api-reference/marketplace.html#add-payment-method) ## Saving a payment method when making a payment You can also save the payment method that the user uses to make a payment, if the user indicates they want to do so. This requires some specific steps when creating the PaymentIntent associated with the payment. ### Check currentUser for an existing saved payment method Before initiating the payment transition, fetch the `currentUser` resource and include the `stripeCustomer.defaultPaymentMethod` relationships to see if the user already has either an associated Stripe Customer or a saved default payment method. Doing this before starting the payment flow means that we can take the correct steps once the payment itself has been handled. ### Pass `setupPaymentMethodForSaving` parameter to payment request When requesting payment i.e. creating the PaymentIntent, the `stripe-create-payment-intent` action requires a specific parameter – `setupPaymentMethodForSaving` – to be passed if the payment method needs to be saved at the same time. If you don't pass this parameter in the transition API call, then the payment method in question cannot be saved to the user in the later step. In the Sharetribe Web Template this is handled by default in the first step of the [_processCheckoutWithPayment_ function](https://github.com/sharetribe/web-template/blob/main/src/containers/CheckoutPage/CheckoutPageTransactionHelpers.js#L205-L208). Read more about the stripe-create-payment-intent action and its parameters: - [stripe-create-payment-intent](https://www.sharetribe.com/docs/references/transaction-process-actions/#actionstripe-create-payment-intent) ### Process the next steps of the transaction Storing a payment method does not affect the following steps of handling the payment, such as making the `stripe.confirmCardPayment` call, triggering the `confirm-payment` transition, and possible other actions you need to process. You can handle them as you would in a regular transaction. However, you will need to access the PaymentIntent resource of the transaction, either by fetching it from Stripe or retrieving it from an API response when confirming the payment with Stripe. You will use the `payment_method` attribute of the PaymentIntent when associating the payment method with a Stripe Customer in the next step. ### Create a Stripe customer and attach the payment method In step 1, you fetched the `currentUser` resource to see if the user already has either an associated Stripe Customer or a saved default payment method. To save the payment method from the PaymentIntent to the current user, the user needs to have a Stripe Customer associated with their profile. The payment method ID of the PaymentIntent is in the attribute `PaymentIntent.payment_method`. - If the user does not have a stripeCustomer attached, use the Sharetribe API `stripeCustomer.create` endpoint and pass the payment method id as the `stripePaymentMethodId` parameter. - If the user already has a `stripeCustomer`, but they don't have a `defaultPaymentMethod`, you can use `stripeCustomer.addPaymentMethod` to associate the the payment intent's payment method id with the user. - If the user has a `stripeCustomer.defaultPaymentMethod`, and you want to replace it with the new payment method, you need to first remove the previous card and then add the new one.
Endpoint(s) **No Stripe Customer** [stripeCustomer.create](https://www.sharetribe.com/api-reference/marketplace.html#create-stripe-customer) **Existing Stripe Customer, no `defaultPaymentMethod`** [stripeCustomer.addPaymentMethod](https://www.sharetribe.com/api-reference/marketplace.html#add-payment-method) **Existing Stripe Customer and `defaultPaymentMethod`** [stripeCustomer.deletePaymentMethod](https://www.sharetribe.com/api-reference/marketplace.html#delete-payment-method), [stripeCustomer.addPaymentMethod](https://www.sharetribe.com/api-reference/marketplace.html#add-payment-method) Once you set up your payment flow to properly save a card with the Payment Intents or Setup Intents API, Stripe will mark any subsequent off-session payment as a merchant-initiated transaction to reduce the need to authenticate. Merchant-initiated transactions require an agreement (also known as a "mandate") between you and your customer. Read more about merchant-initiated transactions in Stripe's documentation: - [Customer-Initiated Transactions (CIT) and Merchant-Initiated Transactions (MIT)](https://docs.stripe.com/payments/cits-and-mits). ### Charging saved cards If the user has a default payment method and they choose to use it to make a payment, you need to make the following adjustments to the default payment initiation. First, when you call the transition that has `stripe-create-payment-intent` and want to use a customer's saved payment method, you need to pass the payment method id as an API call parameter to use the saved payment method. Read more about the stripe-create-payment-intent action and its parameters: - [stripe-create-payment-intent](https://www.sharetribe.com/docs/references/transaction-process-actions/#actionstripe-create-payment-intent) Second, when you are confirming the payment intent toward Stripe with `stripe.confirmCardPayment`, you do not need to pass the card element parameter since you are using the default payment method. Read more about `stripe.confirmCardPayment` in Stripe's own documentation: - [stripe.confirmCardPayment](https://docs.stripe.com/js/payment_intents/confirm_card_payment) ## Frequently asked questions about storing payment cards ### How many payment cards can I store per customer? Right now you can only store one payment card per customer. If you store a new card, the old one is replaced. ### How do I edit the details of a stored credit card? You can't edit the details of a stored payment card. Instead, you need to delete the card and create a new card. ### Can I create delayed charges? Sometimes you might want to store the payment card details of the customer when they make a booking, but initiate the actual charge only later. A typical example could be booking a venue for a wedding. The initial booking might be done a year in advance, but the charge might happen only a bit before the event, or even after it. It's possible for you to adjust your [transaction process](/concepts/transactions/transaction-process/) to add a transition that attempts to automatically charge the card of the customer at a specific point in time. This is called an off-session payment, and we have a separate article describing how you can build such a process. Read more about automatic off-session payments: - [Automatic off-session payments in the transaction process](/concepts/payments/off-session-payments-in-transaction-process/) ### Can I create extra charges to the payment card of the customer? Sometimes you might want to create extra charges for a stored card after an initial purchase. For example, if a rented item is stolen or damaged by the customer or returned late, you might want to charge the customer extra to cover these costs. If your marketplace uses [Strong Customer Authentication](/template/payments/strong-customer-authentication/) to verify credit card purchases, you cannot create additional charges to their card without allowing them to approve the charge with Strong Customer Authentication. If that is not the case, you can initiate additional charges directly from your Stripe dashboard. You should always notify the customer in question about why an extra charge was placed on their card. The additional charges won't get displayed in Sharetribe Console. The money from the extra charges is placed to your platform's Stripe balance, from which it is moved to your bank account. If a payout to the provider (in this case the owner of the item) is needed, you will need to handle it manually from your own bank account. ### Can I enable recurring / subscription payments? Stripe supports [subscriptions](https://stripe.com/docs/connect/subscriptions), which could be used to allow your providers to charge recurring payments from your customers. As an example, a customer might rent a storage unit from a provider. You might want to create a subscription that automatically charges the customer's card every month, until the customer cancels the storage subscription. Right now, Sharetribe doesn't offer support for Stripe subscriptions. However, there is a workaround with custom development. Once the customer has made the initial payment, you would send a request to your own backend component, for instance the server of your Sharetribe Web Template, which would then create a subscription with the stored credit card of the customer. The subsequent subscription payments would then not be visible in Sharetribe Console, but you could monitor them from Stripe dashboard. --- ## Commissions and monetizing your platform Path: concepts/pricing-and-commissions/commissions-and-monetizing-your-platform/index.mdx # Commissions and monetizing your platform ## Introduction If you have already defined a pricing model for your marketplace, this article will provide you with basic information on the options Sharetribe provides and how to take them into use. If you need more information on how to decide the pricing, our Marketplace Academy has [an article](https://www.sharetribe.com/academy/how-to-set-pricing-in-your-marketplace/) describing different pricing models and the tradeoffs behind different options. As background, familiarizing yourself with [line items](/concepts/pricing-and-commissions/pricing/#line-items) and [privileged transitions](/concepts/transactions/privileged-transitions/) gives you a good understanding of the concepts discussed in this article. In addition, the article that describes [payments in Sharetribe](/concepts/payments/payments-overview/) provides valuable information about how the payment flow in Sharetribe works. Configuring commissions happens with the [privileged-set-line-items](/references/transaction-process-actions/#actionprivileged-set-line-items) transaction process action. In the template, this is done [on the server side](https://github.com/sharetribe/web-template/blob/main/server/api-util/lineItems.js) because of the privileged nature of this action. If you are developing a client application that is not based on the Sharetribe Web Template, you can apply a similar logic. ## Percentage-based commissions One of the simplest ways to configure commissions is to define a percentage of the listing price as a commission. Commissions can be charged from the provider, the customer, or both. If your application uses hosted configurations, you can define provider and customer commission percentages and minimum commission amounts in Sharetribe Console. ![Commission defined in Console](./consoleCommission.png) To implement the other use cases in this article, you will need a degree of custom development. _This example illustrates how to use percentage-based commissions without Console-based hosted configurations._ A marketplace that charges 10 % from the customer and 12 % from the provider would configure the commissions in code like this: ```jsx filename="lineItems.js" const { calculateTotalFromLineItems } = require('./lineItemHelpers'); const PROVIDER_COMMISSION_PERCENTAGE = -12; // Provider commission is negative const CUSTOMER_COMMISSION_PERCENTAGE = 10; // Customer commission is positive const order = { code, unitPrice, quantity, includeFor: ['customer', 'provider'], }; const providerCommissionPercentage = { code: 'line-item/provider-commission', unitPrice: calculateTotalFromLineItems([order]), percentage: PROVIDER_COMMISSION_PERCENTAGE, includeFor: ['provider'], }; const customerCommissionPercentage = { code: 'line-item/customer-commission', unitPrice: calculateTotalFromLineItems([order]), percentage: CUSTOMER_COMMISSION_PERCENTAGE, includeFor: ['customer'], }; const lineItems = [ order, providerCommissionPercentage, customerCommissionPercentage, ]; ``` For a 100 EUR listing, this would result in a 110 EUR payin for the customer and a 88 EUR payout for the provider. The marketplace would receive 22 EUR minus Stripe fees.
Negative or positive commission? Commission line items are defined as either positive or negative depending on the transaction party. - Provider commission is defined as **negative**, since the provider's total is the listing price minus the provider commission. ![Provider commission](./provider_commission.png) - Customer commission is defined as **positive**, since the customer's total is the listing price plus the customer commission. ![Customer commission](./customer_commission.png)
## Fixed commissions In addition to percentages, you can define commissions with fixed sums as the `unitPrice` of the line item using `quantity` instead of `percentage`. In the following example, both the provider and customer pay a fixed commission regardless of the listing price or quantity, and Console-based commissions are ignored. ```jsx filename="lineItems.js" const FIXED_PROVIDER_COMMISSION = -1500; // Provider commission is negative const FIXED_CUSTOMER_COMMISSION = 1050; // Customer commission is positive const calculateCommission = (unitPrice, amount) => { return new Money(amount, unitPrice.currency); }; const order = { code, unitPrice, quantity, includeFor: ['customer', 'provider'], }; const providerCommissionFixed = { code: 'line-item/provider-commission', unitPrice: calculateCommission(unitPrice, FIXED_PROVIDER_COMMISSION), quantity: 1, includeFor: ['provider'], }; const customerCommissionFixed = { code: 'line-item/customer-commission', unitPrice: calculateCommission(unitPrice, FIXED_CUSTOMER_COMMISSION), quantity: 1, includeFor: ['customer'], }; const lineItems = [ order, ...extraLineItems, providerCommissionFixed, customerCommissionFixed, ]; ``` For a 100 EUR listing, this would result in a 110.5 EUR payin for the customer and a 85 EUR payout for the provider. The marketplace would receive 25.5 EUR minus Stripe fees. ## Dynamically calculated commissions You can also calculate the commissions with more complex logic. You can set the result of the calculation as either the `unitPrice` or the `percentage` of the line item. In this example, the customer's commission percentage gets reduced with three percentage points (e.g. from 10 per cent down to 7 per cent) when they buy 5 items or more. The provider's commission is percentage based, but always at least 10 dollars. ```jsx filename="lineItemHelpers.js" // Base provider and customer commissions are fetched from assets const MINIMUM_PROVIDER_COMMISSION = -1000; // Negative commission in minor units, i.e. in USD cents const CUSTOMER_COMMISSION_PERCENTAGE_REDUCTION = 3; const calculateProviderCommission = (order, providerCommission) => { // Use existing helper functions to calculate totals and percentages const price = this.calculateTotalFromLineItems([order]); const commission = calculateTotalPriceFromPercentage( price, providerCommission ); // Since provider commissions are negative, comparison must be negative as well if (commission.amount < MINIMUM_PROVIDER_COMMISSION) { return commission; } return new Money(MINIMUM_PROVIDER_COMMISSION, price.currency); }; const calculateCustomerCommissionPercentage = ( order, customerCommission ) => order.quantity > 4 ? customerCommission.percentage - CUSTOMER_COMMISSION_PERCENTAGE_REDUCTION : customerCommission.percentage; exports.getDynamicProviderCommissionMaybe = ( order, providerCommission ) => this.hasCommissionPercentage(providerCommission) ? [ { code: 'line-item/provider-commission', unitPrice: calculateProviderCommission( order, getNegation(providerCommission.percentage) ), quantity: 1, includeFor: ['provider'], }, ] : []; exports.getDynamicCustomerCommissionMaybe = ( order, customerCommission ) => this.hasCommissionPercentage(customerCommission) ? [ { code: 'line-item/customer-commission', unitPrice: this.calculateTotalFromLineItems([order]), percentage: calculateCustomerCommissionPercentage( order, customerCommission ), includeFor: ['customer'], }, ] : []; ``` Remember that the amount defined in the Money object [must be an integer](/concepts/pricing-and-commissions/pricing/#money-type-format). Then you can use the function like this in _lineItems.js_: ```js filename="lineItems.js" const { getDynamicProviderCommissionMaybe, getDynamicCustomerCommissionMaybe, } = require('./lineItemHelpers'); const order = { code, unitPrice, quantity, includeFor: ['customer', 'provider'], }; // Let's keep the base price (order) as first line item and provider and customer commissions as last. // Note: the order matters only if OrderBreakdown component doesn't recognize line-item. const lineItems = [ order, ...extraLineItems, ...getDynamicProviderCommissionMaybe(order, providerCommission), ...getDynamicCustomerCommissionMaybe(order, customerCommission), ]; ``` ## Subscription-based model The line item commissions are the most straightforward way of monetizing your marketplace and are directly supported by Sharetribe. However, you might want to experiment with other monetization models depending on your business idea. For example, subscriptions might be a good way of monetizing your marketplace. With the [Integration API](/concepts/api-sdk/marketplace-api-integration-api/#when-to-use-the-integration-api), you can integrate a third-party service such as [Chargebee](https://www.chargebee.com/) or [Stripe billing](https://stripe.com/en-fi/billing) to process subscription payments from users who want access to your marketplace. --- ## Pricing Path: concepts/pricing-and-commissions/pricing/index.mdx # Pricing ## What kind of pricing can you achieve with Sharetribe It's common for a marketplace to base it's pricing on the length of a booking, on a number of booked units, or the combination of these two. With Sharetribe you can design your pricing using these two parameters but the pricing can also be extended to support more complicated business models or even be replaced with a completely different model altogether. In this article, we'll look a bit closer at how pricing works in Sharetribe and how pricing schemes can be designed. ## Pricing terminology - **Line item:** Something that affects transaction price, for example, a booking for two nights, a cleaning fee, or a customer commission. Think of as a line in a receipt. - **Line total:** Total price of a line item. - **Payin total:** Amount of money that a customer pays for a transaction. The value is the sum of line items that apply to the customer. - **Payout total:** Amount of money a provider receives from a transaction. The value is the sum of line items that apply to the provider. ## Money type format Following the Sharetribe SDK documentation, you will see that the [Money type](https://sharetribe.github.io/flex-sdk-js/types.html) requires both an amount and a currency, with the amount expressed as cents. See our documentation on [currency subunits](https://www.sharetribe.com/docs/template/configuration/how-to-set-up-currency-in-template/#currency-subunits) for more information. `new Money(amount: Number, currency: String)` When working with the [APIs](https://www.sharetribe.com/api-reference/) and in the Template, it is important to ensure that the amount is an integer, as floating point numbers are not supported. ## Line items In Sharetribe, the total price of a transaction is defined by its _line items_. Line items describe what is included in a transaction. It can be a varying set of things from the number of booked units to customer and provider commissions, add-ons, discounts, or payment refunds. A transaction gets its price from the [privileged-set-line-items](/references/transaction-process-actions/#actionprivileged-set-line-items) action . The action takes a list of line items as a parameter. Remember, that the `privileged-set-line-items` action needs to be placed in [a privileged transition](/concepts/transactions/privileged-transitions/). Every line item has a unit price and one of the following attributes: _quantity_ or _percentage_. The quantity attribute can be used to denote the number of booked units, like the number of booked nights. Quantity can also be defined as a multiplication of _units_ and _seats_. The percentage param is used when modeling commissions for example. Based on these attributes a line total is calculated for each line item. Line totals then define the total payin and payout sums of the transaction. The following arguments can be passed in a line item to the Sharetribe API: - `code`: A string that identifies the line item. Must start with `line-item/`, for example, `line-item/cleaning-fee`, mandatory. - `unitPrice`: Price of a single unit of the line item, mandatory. - `lineTotal`: Total value of the line item, can be negative. - `quantity`: Total amount of units. Can be defined explicitly or calculated by multiplying `units` and `seats`. - `percentage`: A percentage that is used to calculate the line total. - `seats`: Number of seats that are used to calculate the quantity. - `units`: Number of units. In combination with seats, forms quantity. - `includeFor`: An array containing strings `customer` and/or `provider`. Defines which party of a transaction the line item applies to. The `lineTotal` is not a mandatory parameter. Sharetribe will calculate the line total and if one is provided, it will validate the `lineTotal` parameter against the calculated value. ## Calculating the price The price of a line item can be calculated in three ways, combining the `unitPrice` with either `quantity`, `seats` and `units`, or `percentage`. The following tables provide examples of all price calculation types: ### unitPrice with quantity Use unit price and quantity to calculate the price when you want to multiply a fixed fee with a quantity. | code | unitPrice | quantity | lineTotal | includeFor | | :------------------------------------ | :--------------------- | :------- | :---------------------- | :----------------------- | | "line-item/nights" | new Money(5000, "USD") | 3 | new Money(15000, "USD") | ["customer", "provider"] | | "line-item/cleaning-fee" | new Money(7500, "USD") | 1 | new Money(7500, "USD") | ["customer", "provider"] | | "line-item/fixed-customer-commission" | new Money(2500, "USD") | 1 | new Money(2500, "USD") | ["customer"] | `lineTotal` is calculated as `unitPrice * quantity`. ### unitPrice with seats and units Use unit price combined with seats and units when the number of participants affects the price. | code | unitPrice | seats | units | lineTotal | includeFor | | :----------------- | :--------------------- | :---- | :---- | :---------------------- | :----------------------- | | "line-item/nights" | new Money(5000, "USD") | 3 | 2 | new Money(30000, "USD") | ["customer", "provider"] | `lineTotal` is calculated as `unitPrice * seats * units`. ### unitPrice and percentage Use unit price and percentage when the line total is calculated as a percentage of some other (sub)total. | code | unitPrice | percentage | lineTotal | includeFor | | :------------------------------ | :---------------------- | :--------- | :---------------------- | :----------------------- | | "line-item/coupon-discount" | new Money(50000, "USD") | -15 | new Money(-7500, "USD") | ["customer", "provider"] | | "line-item/customer-commission" | new Money(50000, "USD") | 15 | new Money(7500, "USD") | ["customer"] | | "line-item/provider-commission" | new Money(50000, "USD") | -15 | new Money(-7500, "USD") | ["provider"] | `lineTotal` is calculated as `unitPrice * percentage / 100`. ## Refunds Refunds are created with the [calculate-full-refund](/references/transaction-process-actions/#actioncalculate-full-refund) action. It sets transaction pay in and pay out amounts to zero and creates reverse line items that undo all the previous line items. Note, that the `calculate-full-refund` action can be run only once during a transaction. The action calculates a full refund. Partial refunds are not supported by Sharetribe at the moment. ## Price negotiation You may want to allow your users to negotiate a price for a transaction instead of setting a default listing price that determines the transaction payment. In a negotiation flow, the customer or provider starts the transaction with requesting or suggesting a quote, and the other party can then either accept or reject the quote, or make a counter offer. This quote is set as the main line item of the transaction instead of the default listing price. Once both parties have accepted a quote, the negotiation completes with the customer making a payment. Your potential customer and provider commissions will be applied in addition to the negotiated quote to form the payin and payout totals of the transaction. ## Example pricing options ### Add-ons Upsell additions on top of the regular price. Examples: cleaning fee, insurance, delivery, longer checkout on a booking. ```jsx const lineItems = [ { code: 'line-item/night', unitPrice: { amount: 5000, currency: 'EUR', }, lineTotal: { amount: 5000, currency: 'EUR', }, includeFor: ['customer', 'provider'], quantity: 1, }, // Upsell item: longer checkout on a booking for a 30% increase on the listing price { code: 'line-item/longer-checkout', unitPrice: { amount: 5000, currency: 'EUR', }, lineTotal: { amount: 1500, currency: 'EUR', }, includeFor: ['customer', 'provider'], quantity: 0.3, }, ]; ``` ![Line items for longer checkout in Console](./longer-checkout-lineitems.png) ### Discount based on booking length An example: daily price is $20, weekly price $70, and monthly price $200. Another example: 20% discount on daily rate for bookings of 5 or more days, 30% discount on bookings of 10 or more days, and so on. ```jsx filename="lineItems.js" const quantityOrSeats = !!units && !!seats ? { units, seats } : { quantity }; // A user has saved a weekly price in the listing const { weeklyPrice } = listing.attributes.publicData; const weeklyUnitPrice = new Money( weeklyPrice.amount, weeklyPrice.currency ); const useWeeklyUnitPrice = !!units ? units >= 7 : quantity ? quantity >= 7 : false; const order = { code, unitPrice: useWeeklyUnitPrice ? weeklyUnitPrice : unitPrice, ...quantityOrSeats, includeFor: ['customer', 'provider'], }; ``` ### Taxes Add any type of tax rate to the listing price and display them as separate line items in the receipt. ```jsx const lineItems = [ { code: 'line-item/day', unitPrice: { amount: 4500, currency: 'EUR', }, lineTotal: { amount: 9000, currency: 'EUR', }, reversal: false, includeFor: ['customer', 'provider'], seats: 1, units: 2, }, // Value added tax at 24% { code: 'line-item/value-added-tax', unitPrice: { amount: 9000, currency: 'EUR', }, lineTotal: { amount: 2160, currency: 'EUR', }, includeFor: ['customer', 'provider'], quantity: 0.24, }, ]; ``` ![Line items with tax](./tax-line-items.png) ### Seasonal pricing Your users can, for example, define weekends to cost more than weekdays, or summers to cost more than winters. You can either allow your users to determine their own seasonal periods, or set them yourself. ```jsx filename="lineItems.js" // User has set seasonal months to be June, July, and December, // and bookings in those months use an increased seasonal price const seasonalMonths = [5, 6, 11]; const seasonalPrice = new Money( unitPrice.amount * 1.2, unitPrice.currency ); const { bookingStart, bookingEnd } = orderData; const bookingStartMonth = new Date(bookingStart).getUTCMonth(); const bookingEndMonth = new Date(bookingEnd).getUTCMonth(); const useSeasonalPrice = seasonalMonths.includes(bookingStartMonth) && seasonalMonths.includes(bookingEndMonth); const quantityOrSeats = !!units && !!seats ? { units, seats } : { quantity }; const order = { code, unitPrice: useSeasonalPrice ? seasonalPrice : unitPrice, ...quantityOrSeats, includeFor: ['customer', 'provider'], }; ``` ### Quantity discount An example: booking a room for two people costs $100, and after that, each additional person costs $20 extra. ```jsx filename="lineItems.js" // User has defined the basic seat count for the facility as 2, and additional guests // are charged a lowered price. const basicSeatsCount = 2; const additionalSeatsPrice = new Money( unitPrice.amount * 0.2, unitPrice.currency ); const hasAdditionalSeats = !!seats && seats > basicSeatsCount; const quantityOrSeats = !!units && !!seats ? { units, seats: hasAdditionalSeats ? basicSeatsCount : seats } : { quantity }; const additionalUnitsAndSeats = hasAdditionalSeats && { units, seats: seats - basicSeatsCount, }; const order = { code, unitPrice, ...quantityOrSeats, includeFor: ['customer', 'provider'], }; const additionalSeatsLineItemMaybe = hasAdditionalSeats ? [ { code: 'line-item/additional-guests', unitPrice: additionalSeatsPrice, ...additionalUnitsAndSeats, includeFor: ['customer', 'provider'], }, ] : []; ``` ![Line items for additional guests at a lowered price](./additional-guests-line-items.png) ### Price variants With bookings, you can enable price variants. You could add two variants of a single listing, e.g. standard and premium versions, which have a price difference. When using the Sharetribe Web Template, price variations can be configured in Console when creating listing types. Read more in our Help Center: - [How price variations work](https://www.sharetribe.com/help/en/articles/11124054-how-price-variations-work) --- ## Negotiation process Path: concepts/transactions/negotiation-process/index.mdx # Negotiation process Sharetribe has a _default-negotiation_ transaction process that includes price negotiation dynamics. The same process enables both regular and reverse transaction flows. ## **Regular transaction flow** A regular transaction flow refers to a traditional marketplace transaction flow. This happens on a marketplace when the provider posts a listing and a customer can initiate a transaction against that listing. A transaction process supports the regular flow when it has one or more initial transitions defined for the customer. When a transaction is initiated using a customer transition, it follows a regular transaction flow. The _default-negotiation_ process has one initial transition defined for a customer: ```clj filename="process.edn" {:name :transition/request-quote, :to :state/quote-requested, :actor :actor.role/customer, :actions [{:name :action/update-protected-data}]} ``` ## **Reverse transaction flow** A reverse transaction flow on a marketplace happens when the customer posts a listing and a provider can initiate a transaction against that listing. This means that the initiating transition for the transaction is made by the provider. A transaction process supports the reverse flow when it has one or more initial transitions defined for the provider. When a transaction is initiated using a provider transition, it follows a reverse transaction flow. The _default-negotiation_ process has two initial transitions defined for a provider: _transition/make-offer_ and _transition/inquire_: ```clj filename="process.edn" {:name :transition/make-offer, :to :state/offer-pending, :actor :actor.role/provider, :actions [{:name :action/privileged-set-line-items} {:name :action/update-protected-data} {:name :action/privileged-update-metadata}], :privileged? true} {:name :transition/inquire, :to :state/inquiry, :actor :actor.role/provider, :actions []} ``` Sharetribe Console supports creating listing types with the reverse negotiation flow starting in October 2025, and listing types with the regular negotiation flow starting in November 2025. The Sharetribe Web Template supports starting and processing transactions with reverse negotiation listing types starting with version [9.0.0](https://github.com/sharetribe/web-template/releases/tag/v9.0.0), and with regular negotiation listing types starting with version [10.1.10](https://github.com/sharetribe/web-template/releases/tag/v10.1.0). In both regular and reverse flows, the provider provides the product or service in question, and the customer pays for the transaction.
No bookings or stock by default The _default-negotiation_ process does not include bookings or stock. You can customize the process to include availability handling or stock handling in addition to price negotiation. For bookings, we also offer an example transaction process [negotiated-booking](https://github.com/sharetribe/example-processes/blob/master/README.md#negotiated-booking) – it is not supported by default in the Sharetribe Web Template, so you will need to build a client-side implementation.
The default negotiation process is best suited for use cases where the customer and provider negotiate both the scope and the price of the transaction, such as a custom built item or a complex project. The negotiation process has four main phases: 1. Transaction initiation 2. Negotiation loop 3. Change request loop 4. Reviews ![Full negotiation process](./negotiation-full-process-highlight-phases.png) In addition to transitions that process in a linear manner, it is also possible to build loops in a transaction process. A loop in a transaction process means that the same state may be entered multiple times. Loops make it possible for the participants to go back and forth in the process, or for either participant to take the same action multiple times. The default-negotiation process has two main loops: the [negotiation loop](#negotiation-loop) and the [change request loop](#change-request-loop). In addition, the negotiation loop contains a sub-loop where a provider can update their offer multiple times before the customer has accepted it. With all transaction process loops, including the ones in this process, it is important to keep in mind that a single transaction can only have a maximum of 100 transitions. This means that any implementation you create must make sure that you restrict looping transitions after a certain threshold, because your users should always have enough transition quota left to finish the transaction. The Sharetribe Web Template default implementation for the reverse negotiation flow takes this limitation into account. It [disables the customer's counter offer button after 50 transitions](https://github.com/sharetribe/web-template/blob/main/src/containers/TransactionPage/TransactionPage.stateDataNegotiation.js#L86-L117), and from [requesting changes to the order after 90 transitions](https://github.com/sharetribe/web-template/blob/main/src/containers/TransactionPage/TransactionPage.stateDataNegotiation.js#L183-L218). ## Transaction initiation First, either the customer or the provider initiates the transaction. Transaction initiation does not have looping logic. In a regular flow, a customer can start the transaction against the provider's listing by requesting a quote. The provider can then submit an offer from the request, and submitting an offer [adds line items](/references/transaction-process-actions/#actionprivileged-set-line-items) to the transaction. Alternatively, the provider (or operator) can reject the quote request. The customer can also withdraw their own quote request. ![Customer requests a quote](./request-quote-details.png) In the transaction process graphs in this article and in Console, orange arrows show transitions defined for the customer, and purple arrows show transitions defined for the provider. Green arrows show transitions defined for the operator, and grey arrows show automatic transitions. In a reverse flow, a provider can start the transaction against the customer's listing in two ways: - the provider can inquire, which does not add line items to the transaction - or the provider can submit an offer, which does add line items to the transaction. ![Provider submits offer](./make-offer-details.png) ## Negotiation loop After the transaction has been initiated and the provider has submitted an offer, both regular and reverse transactions proceed in the same way. The negotiation phase loops between two states: - _state/offer-pending_ - _state/customer-offer-pending_ ![Negotiation loop states](./customer-provider-offers-transitions-highlight-states.png) From _state/offer-pending_, there are multiple paths for both the customer and the provider. Even though technically the parties can use the looping transitions to negotiate the price, in practice we recommend that the participants exchange messages to land on the quote and other offer details. That way, they don't use up the allowed transaction quota during the negotiation phase. When there is a pending offer from the provider (_state/offer-pending_), the customer can continue the transaction in two ways: either - accept the offer and make a payment, which exits the negotiation loop - or make a counter-offer and suggest new line items for the transaction. The customer or operator can also end the transaction by rejecting the offer from _state/offer-pending_. ![Customer options from state/offer-pending](./customer-provider-offers-transitions-highlight-customer-path.png) The provider can withdraw or update their offer from _state/offer-pending_. If the provider updates their offer, the transaction goes to _state/update-pending_. From this state, the provider can re-update their offer or withdraw it. ![Provider transitions around update offer](./update-offer-transitions-highlight-provider-path.png) The customer can either accept the update or reject the new offer from _state/update-pending_. The operator can also accept an update to the offer. Accepting the offer transitions the transaction back to _state/offer-pending_ and the main negotiation loop. ![Customer transitions around update offer](./update-offer-transitions-highlight-customer-path.png) If a customer makes a counter offer from _state/offer-pending_ and moves the transaction to _state/customer-offer-pending_, the provider can continue the transaction in three ways, all of which transition the transaction back to _state/offer-pending_: - they can make a new counter offer - they can accept the customer's counter offer - or they can reject the customer's counter offer ![Provider paths from customer counter offer](./customer-provider-offers-transitions-highlight-provider-paths-from-customer-offer.png) The customer can withdraw their counter offer, which also transitions the transaction back to _state/offer-pending_. The only way to end the transaction from _state/customer-offer-pending_ is the operator transition _operator-reject-from-customer-counter-offer_ ![Customer paths from customer counter offer](./customer-provider-offers-transitions-highlight-customer-path-from-customer-offer.png) When the customer and provider are both happy with the offer, the customer can initiate payment with the current quote, i.e. the line items set for the transaction. Once the payment has been confirmed, the transaction moves to the second main loop, which is the change request loop. ## Change request loop After the payment step, the transaction now moves on into the delivery and change request phase. ![Change request loop](./change-request-loop-full.png) When the provider delivers the order, or the operator marks the order as delivered, the transaction transitions to _state/delivered_. The transaction can now loop between two states: - _state/delivered_ - _state/changes-requested_ ![Change request loop](./change-request-loop-highlight-states.png) The customer and operator can now - accept the order, and transition the transaction to _state/completed_ - or they can request changes by transitioning the transaction to _state/changes-requested_ ![Customer paths in change request loop](./change-request-loop-highlight-customer-paths.png) The details of the change request will need to be discussed in messages, since the change request transition does not have any actions to store the change request information. Once changes have been requested, the provider has only one transition available – to deliver the changes and transition the transaction back to _state/delivered_. ![Provider path in change request loop](./change-request-loop-highlight-provider-path.png) In addition, there are multiple operator transitions and automatic transitions at play. - If the provider doesn't deliver the order in 75 days from the payment, the transaction is automatically canceled with _auto-cancel_. The operator can also manually cancel the transaction before the order is delivered with _operator-cancel_. This ends the transaction. - Once the order has been marked as delivered, the operator can cancel the transition with _operator-cancel-from-delivered_. - Once a customer has requested changes, the operator can cancel the transaction with _operator-cancel-from-changes-requested_. If the change request is still pending in 75 days from the payment, the transaction is automatically canceled with _auto-cancel-from-changes-requested_. All of these paths transition the transaction to _state/canceled_ ![Operator and automatic transitions in change request loop](./change-request-loop-highlight-operator-transitions.png) If the customer doesn't request changes, and the transaction has remained in _state/delivered_ for 14 days, the transaction automatically transitions to _state/completed_. ## Reviews After the transaction is in _state/completed_, the participants can then review each other similarly to other transaction processes. The review phase does not have looping logic. ![Review transitions](./review-transitions.png) Both parties are invited to post a review once the transaction completes. Whoever posts their review first, the other participant is notified that they still need to post their review. Reviews are only published once both parties have posted their review, or once the review period expires and the participants can no longer submit a review. --- ## Privileged transitions Path: concepts/transactions/privileged-transitions/index.mdx # Privileged transitions ## What are privileged transitions? In Sharetribe, a process transition is an edge between two states in the transaction process graph. Invoking transitions is guarded in the process definition by tying them to a specific state when they can be transitioned and by defining who can perform the transition. This way transition requests have built-in validation of who can invoke them and in what state of the transaction flow. However, there are moments when more control is required on who can initiate a transition and especially with what kind of parameters. Take discounts on pricing by leveraging discount coupons managed by a 3rd party service for example. A coupon code can be validated in the client side by invoking the coupon service but this will not limit who will be technically able to invoke a pricing related transition with discounted price parameters. This is where privileged transitions come into play. They are transaction process transitions that can be invoked only from a trusted context. In other words, this means that you can build your own server side validation that sits between your marketplace UI and the Sharetribe Marketplace API. In the discount coupon example, this means that the discount coupon that a user has can be passed as a parameter in the transition request. Server side transition request validation can invoke a 3rd party service to verify a discount code, update pricing parameters accordingly, and pass those to the transition that has pricing actions tied to it. ## How do privileged transitions work? Standard authenticated Marketplace API requests require a valid access token obtained from the Authentication API. Privileged transitions differ here by requiring a special kind of _trusted_ token to authenticate properly. A trusted token can be obtained by exchanging a valid access token to a trusted one in the Authentication API by providing a client secret. The client secret is not to be exposed publicly, securing that privileged transitions can only be invoked by a trusted source, i.e. your own backend implementation. Attempting to initiate or transition a transaction with a privileged transition with a non-trusted access token (either a valid anonymous token or a valid user access token) will throw a 403 Forbidden error. ![Authentication flow with a trusted access token](./auth-flow.png) The client secret is tied to a Marketplace API [application](/concepts/development/applications/). When exchanging an access token to a trusted one, the client secret needs to be from the same application as the client ID that was used to obtain the access token. Remember to never expose the client secret publicly. Doing so would enable full control over requests that invoke privileged transitions. ## How to use a privileged transition A privileged transition is defined by setting a `privileged?` attribute to `true` for the given transition in a transaction process as follows: ```clojure filename="process.edn" {:name :transition/request-payment :actor :actor.role/customer :actions [{:name :action/create-pending-booking} {:name :action/privileged-set-line-items} {:name :action/stripe-create-payment-intent}] :to :state/pending-payment :privileged? true} ``` Privileged transitions are configured just like a normal transitions. However, what's special about privileged transitions is that, unlike normal transitions, they can contain _privileged actions_. Privileged actions validate that they are always used in a _trusted context_ and usually handle sensitive information. An example of such action is [privileged-set-line-items](/references/transaction-process-actions/#actionprivileged-set-line-items) which allows full control over the price of a transaction, including the commission. ## Operator transitions in the Integration API The Integration API [makes it possible to invoke transitions](https://www.sharetribe.com/api-reference/integration.html#transition-transaction) for which the `:actor` is set to `:actor.role/operator`. As the Integration API authentication requires knowledge of the integration application's client secret and is meant to be used only from your own backend implementation, it is considered a trusted source for invoking transitions. As a consequence, the operator transitions can utilize any privileged actions. For instance, [privileged-update-metadata](/references/transaction-process-actions/#actionprivileged-update-metadata) action can be used to update the transaction's metadata. --- ## Reviews Path: concepts/transactions/reviews/index.mdx # Reviews Sharetribe marketplaces use reviews to create social proof and transparency for the marketplace. Reviews are important for all kinds of marketplaces. Provider reviews encourage customers to order well-rated listings, and customer reviews encourage providers to accept business from well-rated customers. In Sharetribe, reviews can have a rating between 1-5, as well as text content. By default, reviews are given at the end of a transaction. The customer reviews the transaction's listing. Since providers can have several different listings, this way the review targets the actual product or service received. Listing reviews give important information about the quality of the experience and the end result that the customer received during the transaction. The provider reviews the customer directly. Customer reviews are crucial in booking related marketplaces, especially ones where customers interact with the provider directly or use a valuable resource such as an apartment or a car. ## Reviews in a transaction process The default Sharetribe transaction processes use a blind review process. In other words, reviews are only published after both parties have given their review, or after the review period has ended. This increases the odds that the reviews are honest. ### Review related transaction process actions and transitions Reviews are managed as a part of the transaction process using transaction process actions. - [:action/post-review-by-customer](/references/transaction-process-actions/#actionpost-review-by-customer) creates a pending review of the listing and provider. - [:action/post-review-by-provider](/references/transaction-process-actions/#actionpost-review-by-provider) creates a pending review of the customer. - [:action/publish-reviews](/references/transaction-process-actions/#actionpublish-reviews) publishes any reviews that have been added to the transaction by the time the action is executed. The Sharetribe default transaction processes structure these actions into different paths, depending on who posts the first review. In all paths, the process publishes the reviews once both parties have reviewed each other. Alternatively, any existing reviews get published after 7 days of the transaction completing, after which reviews can no longer be added through the transaction process. This means that even if one of the parties does not submit a review, the other review does get published. ## Frequently asked questions ### Can reviews be removed? Once published, reviews cannot be removed. They can, however, be modified by the operator in Sharetribe Console > Manage > [Reviews](https://console.sharetribe.com/reviews). ### Can I add a review after the review period has expired? Sometimes, transaction participants want to add missing reviews after the review period has expired. If the marketplace operators decide that the review is relevant and honest, you can add it manually in Console, from the transaction details. You can [learn more in this article](https://www.sharetribe.com/help/en/articles/13632539-how-to-manually-add-a-review). ### How can I add an average rating attribute to my listings? Listing average ratings are not available in Sharetribe out of the box. However, you can create a feature that listens to [review events](/how-to/events/reacting-to-events/). Whenever there's a new review event, you can then call [Sharetribe Integration API](https://www.sharetribe.com/api-reference/integration.html) to update the [public metadata](/references/extended-data/#metadata) of the reviewed listing. You can create a logic that stores the number of reviews and their previous average in the listing's metadata, and updates these when a new review is added. That way, you can, for instance, display the average rating on the listing page, or use it to sort listings. --- ## Introduction to transaction processes Path: concepts/transactions/transaction-process/index.mdx # Introduction to transaction processes Your marketplace exists to connect supply and demand. There are countless ways of making this connection happen, so Sharetribe allows you to customize your transaction process to make different types of transaction configurations possible. ## Users interact through transactions Any time users connect with each other on a Sharetribe marketplace, they do it through a transaction. At its core, a Sharetribe transaction is the interaction between two users — the provider and the customer — from beginning to end. A single transaction might include events such as messages between users, the payment sent from the customer to the provider, and the reviews users leave about their experience. On different marketplaces, users transact in different ways. For instance, you may want your providers to accept requests before confirming bookings to ensure that there is no conflict. Or, you might prefer requests to confirm automatically because you want to prioritize speed. Perhaps you’d like both options, depending on the nature of the offered service. The guidelines for how you’d like users to transact are established in Sharetribe using a transaction process. Your marketplace’s transaction process determines how your customers and providers move through their transaction. You can also have different transaction processes for different ways of transacting, like renting and buying products, in the same marketplace. A transaction follows the trail established by your transaction process. The transaction process maps the steps your users will complete and the possibilities they have upon reaching each step. Your marketplace can have multiple different transaction processes in use simultaneously. You can see the transaction processes of your marketplace in [Sharetribe Console](https://console.sharetribe.com/advanced/transaction-processes). Typically, all transaction processes are different. These differences can be fundamental and change the logic of the order flow, or they can be small and superficial. An example of a fundamental difference is choosing whether the users book - by night or by day, as when booking a hotel room - by seat, as when booking tickets to an event - by hour, as when booking a hairdresser - or not at all, as when booking is handled outside the marketplace. Another fundamental difference between transaction processes would be the direction of the transaction flow: does your marketplace use - a regular flow, where a provider creates a listing and a customer makes a booking or purchase - or a reverse flow, where a customer creates a listing and a provider submits an offer. Out of the default processes in a Sharetribe marketplace, `default-negotiation` supports both regular and reverse flows. The other default processes only support a regular flow. You can custom develop a process that supports either flow, or both of them. A smaller variation could be, for example, deciding if the provider has to always accept the booking before it can be confirmed or if the booking is automatically confirmed as soon as it is made (i.e. instant booking). Another small variation could be a change of wording in a notification email sent to remind the customer of their upcoming booking. ## Transaction process building blocks Each transaction process guides how your users interact in your marketplace. Each process is built with a few building blocks that describe what is going on. These building blocks are called **states**, **transitions**, and **actions**. Let’s explore a transaction process modeled on AirBnB to learn more about them. ![Default transaction process](./complete-transaction-process.png) In the image above you can see one of the default transaction processes in Sharetribe called "default-booking". It is the same process that you will find, albeit with a different layout, within your Console account's [transaction process page](https://console.sharetribe.com/advanced/transaction-processes). It closely mimics how a customer and a provider transact on AirBnB. From a listing, customers can message a provider or book directly by entering their payment details and authorizing the charge on their card. Providers must then either accept the request, reject the request, or let it expire. After an accepted booking is completed, the customer and provider have a certain period of time to review each other. After this, the reviews are published and the transaction is concluded. The default processes are built in to Sharetribe Web Template. Usually, the easiest way to start defining your own transaction process is by editing the default process. In Sharetribe, transaction processes are written in Clojure's edn format, and there are a handful of examples written in edn in this article. If you are not familiar with edn, you can learn more in this article: - [The edn format](/concepts/development/edn/) ### States The status at any given point in a transaction is called its **state**. The state describes where the users are in their transaction. The Sharetribe default booking process, for example, has a state called _preauthorized_. It signifies that a customer has requested to book a time from the provider’s calendar, and a charge on their credit card has been preauthorized. ![Transaction process states](./transaction-process-states.png) Transaction processes are defined in a **process.edn** file of the process directory. In the process.edn file, states are not defined directly. Instead, they are defined as the _:from_ and _:to_ states of transitions: ```clojure filename="process.edn" {:name :transition/confirm-payment, :actor :actor.role/customer :actions [{:name :action/stripe-confirm-payment-intent}], :from :state/pending-payment ;; :from state describes the initial state of the transition :to :state/preauthorized} ;; to state describes the final state of the transition ``` From the _preauthorized_ state the provider can reject or accept the request, in which case the transaction will transition to the _declined_ or _accepted_ state respectively. ```clojure filename="process.edn" {:name :transition/accept :actor :actor.role/provider :actions [{:name :action/accept-booking} {:name :action/stripe-capture-payment-intent}] :from :state/preauthorized :to :state/accepted} {:name :transition/decline :actor :actor.role/provider :actions [{:name :action/decline-booking} {:name :action/calculate-full-refund} {:name :action/stripe-refund-payment}] :from :state/preauthorized :to :state/declined} ``` ### Transitions Transitions move the transaction from one state to another. They are the steps between states. A transition is triggered by one type of user or “actor”: the customer, the provider, the operator, or time (this is known as the “system” actor in Sharetribe, or as an automatic transition). From the accepted state in the transaction, the transaction will automatically transition to a delivered state at a certain point in time. Or, the operator may cancel the booking. Transitions describe the possible next steps from a particular state. They also describe who can complete the steps. If there are no possible transitions from a state, the transaction has ended. It is possible to create loops in a transaction process. For example, in the `default-negotiation` process, transition `transition/customer-make-counter-offer` transitions from `state/offer-pending` to `state/customer-offer-pending`, and `transition/provider-make-counter-offer` transitions from `state/customer-offer-pending` to `state/offer-pending`. This means that the transaction can go between these two states multiple times. A transition can also have a single state as both `:from` and `:to` parameters – for example, `transition/update-from-update-pending` in the `default-negotiation` process both starts and ends in `state/update-pending`. Technically, transitions can be considered the main building blocks of Sharetribe transaction process. They define, implicitly or explicitly, all the other elements of a transaction process. We will not go to into more detail in this article, but you can find more information [in the transaction process format reference documentation.](/references/transaction-process-format/) ![Transaction transitions](./transaction-transitions-v2.png) ```clojure filename="process.edn" ;; These two transitions go from state/preauthorized to state/declined, ;; but one is allowed for the provider, and one is scheduled to run at a specified time {:name :transition/decline :actor :actor.role/provider :actions [{:name :action/decline-booking} {:name :action/calculate-full-refund} {:name :action/stripe-refund-payment}] :from :state/preauthorized :to :state/declined} {:name :transition/expire ;; the :at parameter replaces the :actor parameter in scheduled transitions :at {:fn/min [{:fn/plus [{:fn/timepoint [:time/first-entered-state :state/preauthorized]} {:fn/period ["P6D"]}]} {:fn/plus [{:fn/timepoint [:time/booking-end]} {:fn/period ["P1D"]}]}]} :actions [{:name :action/decline-booking} {:name :action/calculate-full-refund} {:name :action/stripe-refund-payment}] :from :state/preauthorized :to :state/declined} ``` ### Actions Actions describe what happens as part of a transition. For example, the transaction process allows users to transition from accepted state to delivered or cancelled. The transaction may “complete” automatically, or the operator may “cancel” it with the respective transitions. “Complete” actions involve creating a payout to the provider via Stripe (Sharetribe’s payment gateway). “Cancel” actions, on the other hand, include cancelling the booking and issuing a refund. Possible actions are defined by the capacity of the Sharetribe API. The list of all transaction process actions can be found [in the transaction process actions reference documentation](/references/transaction-process-actions/). Creating custom actions is not possible. ![Transaction actions](./transaction-actions-v2.png) ```clojure filename="process.edn" {:name :transition/complete :at {:fn/timepoint [:time/booking-end]} ;; actions: ;; - create Stripe payout to provider :actions [{:name :action/stripe-create-payout}] :from :state/accepted :to :state/delivered} {:name :transition/cancel :actor :actor.role/operator ;; actions: ;; - mark booking as cancelled ;; - calculate refund ;; - issue refund :actions [{:name :action/cancel-booking} {:name :action/calculate-full-refund} {:name :action/stripe-refund-payment}] :from :state/accepted :to :state/cancelled} ``` ### Notifications Notifications specify the content and behavior of emails sent during a transaction.They determine which actor receives them; define what email template is used; and schedule the specific sending time. Email notifications are triggered by the completion of a transition. The email templates used for the notifications are also considered part of the transaction process, and can be fully customised to fit the needs of the marketplace. In the Sharetribe default booking process, transitioning from the accepted to the delivered state triggers three email notifications. The provider receives a notification that their money has been paid out and a notification prompting them to review the customer. The customer receives an email notification to review the provider. To review what notifications are sent as part of the Sharetribe default processes, visit your [Sharetribe Console](https://www.sharetribe.com/help/en/articles/8501778-how-sharetribe-console-works) _> Build > Advanced > Transaction process visualiser,_ and view a specific process version. You can see the notifications associated with each transition in the transition's details by clicking the transition name. ![Notifications for complete transition](./transition-notifications.png) Each included [email notification has a template](/references/email-templates/#editing-transaction-emails) that can be customized using the Sharetribe CLI. You can also edit the content of the transaction notifications in the Console, under Build > Content > Email texts. ![Transaction notifications](./transaction-notifications-v2.png) ```clojure filename="process.edn" {:name :notification/booking-money-paid, :on :transition/complete, :to :actor.role/provider, :template :booking-money-paid} {:name :notification/review-period-start-provider, :on :transition/complete, :to :actor.role/provider, :template :booking-review-by-provider-wanted} {:name :notification/review-period-start-customer, :on :transition/complete, :to :actor.role/customer, :template :booking-review-by-customer-wanted} ``` ## What kind of transaction process customizations are possible? As all marketplaces’ have their own characteristics, it is common to need some customization to the default transaction process to make it more suitable to the customers’ needs. However, quite often it's helpful to start building your process by making slight customizations to the default process. Typical minor customizations for transaction process are adding the possibility for a customer to cancel a booking or a booking request, adding operator transitions, or editing the contents of the email templates used for the notifications. ![An example transaction process with instant booking and customer cancellation](./tx-process-instabook-customer-cancel.png) Another common example is to modify the process so that the provider has to manually mark the booking as completed and the rented goods as returned in good condition. This might be useful in rental marketplaces where the goods that are rented are of high value. In another case, the process could be modified so that the operator can close down the transaction process in case that there is trouble in the order flow. This could be done for example in cases where a customer has booked a time from a professional, but doesn’t appear in the meeting. Apart from the order flow, customizations can also affect the money flow, storing [protected data](/references/extended-data/#protected-data) or sending notifications. ## Start creating your own transaction process The transaction process determines how your users transact on your marketplace. It maps where your users are in a transaction, what possible next steps they have, and how those steps are taken. The transaction process plays out in your marketplace application where your users transact. Now that you understand more about how the transaction process works, it’s time to create your own. Sharetribe provides a few default processes, but you’ll likely want to modify these to capture the unique way your users will transact. You can build your transaction process using the Sharetribe CLI. Here are guides for [creating your own transaction process](/how-to/transaction-process/create-new-transaction-process-with-cli/) and for [getting started with Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/). For more details of the transaction process format, see the [Transaction process format](/references/transaction-process-format/) reference. To customise the UI of your marketplace to match your process changes, see the [Change transaction process setup in Sharetribe Web Template](/how-to/transaction-process/change-transaction-process-in-template/) how-to guide. --- ## Email verification Path: concepts/users-and-authentication/email-verification/index.mdx # Email verification When a user signs up to a Sharetribe marketplace, they must provide an email address. Sharetribe sends a verification email to that address to make sure the person signing up has access to the email inbox. This is done to prevent spam and undelivered messages due to typos and similar issues. The verification token is valid for 48 hours. The authenticated user can request a new token using the [current_user/send_verification_email](https://www.sharetribe.com/api-reference/marketplace.html#send-verification-email) Marketplace API endpoint. The requested tokens remain valid for their respective durations, so requesting a new token does not invalidate previously requested ones. An authenticated user's email verification status is visible in the [`currentUser` resource](https://www.sharetribe.com/api-reference/marketplace.html#currentuser-resource-format) as `currentUser.attributes.emailVerified` (boolean). The user can continue to use the marketplace after signing up whether or not their email address has been verified. While the user's email address is unverified, only a subset of [email notifications](/concepts/messages-notifications/email-notifications/) are sent to the user's email address: - If the user [requests a password change](https://www.sharetribe.com/api-reference/marketplace.html#request-password-reset), the "Reset password" email notification is sent to the address specified in the request, as long as it matches an existing user account, whether or not that address is verified. - If the user then [resets their password](https://www.sharetribe.com/api-reference/marketplace.html#reset-password), the "Password changed" email notification is sent to the user's address, whether or not that address is verified. Other built-in notifications, as well as transaction notifications, are not sent to an unverified user email address. If any notifications were sent to the user's email address while the address was unverified, those notifications will not be sent retroactively after the user verifies their email address. ## End-user email verification The user can verify their email by making an API call to the [current_user/verify_email](https://www.sharetribe.com/api-reference/marketplace.html#verify-email-address) Marketplace API endpoint and passing in the verification token from the "Verify email address" email notification. The email notification is the only way to receive the email verification token. The default "Verify email address" email notification constructs a link to the default route that is used in the Sharetribe Web Template, and passes the verification token as a query parameter. That way, the user can click the link in the notification to verify their email. If you want to create some other way of passing the token to the API endpoint, you will need to modify the "Verify email address" email notification in Console. Read more on how to do that: - [Built-in email notifications](/concepts/messages-notifications/email-notifications/#built-in-email-notifications) ## Operator email verification An operator can also verify a user's email in Console, if they are confident the email address belongs to the user. This is equivalent to the end user verifying their own email address using the token. The [`currentUser` resource](https://www.sharetribe.com/api-reference/marketplace.html#currentuser-resource-format) returned from the API shows both kinds of verifications in the same way. You can find instructions on verifying a user's email address through Console [in our Help Center](https://www.sharetribe.com/help/en/articles/9230296-manage-users#h_1896170a2e). It is imperative to make sure that the operator only verifies a user's email when the user in question has actively provided the verification outside the Sharetribe platform. ## Integration API email verification You can also verify a user's email programmatically using the Integration API [`users/verify_email`](https://www.sharetribe.com/api-reference/integration.html#verify-email-address) endpoint. This bypasses built-in email verification, and therefore, you should only use it if you are able to verify the user's email using other means. This is equivalent to the end user verifying their own email address. In `live` marketplace environments, this endpoint is only available to marketplaces that use their own [SendGrid account for outgoing email](/how-to/emails-and-notifications/set-up-outgoing-email-settings/#using-your-own-sendgrid-account). It is available without this restriction in `dev` and `test` environments. ## SSO email verification In addition to signing up with email and password, users can also create a Sharetribe account using SSO. Read more: - [Social logins & SSO](/concepts/users-and-authentication/social-logins-and-sso/) If a user signs up with SSO using the [current_user/create_with_idp](https://www.sharetribe.com/api-reference/marketplace.html#create-user-with-an-identity-provider) endpoint and the token received from the identity provider has an associated email address, the Sharetribe backend will use the token's `email_verified` status as the Sharetribe account's email verification status. If the request contains an `email` parameter, the IdP token's email verification status is used only if the parameter email address matches the IdP token email address. An example of a Google IdP token payload [decoded with JWT.io](https://www.jwt.io/) showing the `email` and `email_verified` attributes: ```json { "iss": "https://accounts.google.com", "aud": "[randomstring]-[anotherrandomstring].apps.googleusercontent.com", "sub": "130767574011454724420", "email": "example.person@exampledomain.com", "email_verified": true, "name": "Example Person", "given_name": "Example", "family_name": "Person", "iat": 1772180859, "exp": 1772184459 } ``` If the address is verified by the SSO identity provider when the user signs up (i.e. the token at signup contains `"email_verified": true`), then the user does not need to complete a separate email verification. If the address was not verified by the SSO identity provider when the user signed up, the user needs to complete the same email verification as with email signups. Verifying the email address with the identity provider later will not affect the email verification status of the Sharetribe account. If a user has an email address based account with a verified email address, and they log in with an IdP token that has a matching verified email address, the Sharetribe backend connects the IdP login with the existing account. After this first login, the IdP can be used to authenticate to the same account even if one of the email addresses later changes. However, if the email address in either the Sharetribe account or the IdP token is not verified, an attempt to login with an IdP token using a matching email address throws a 401 Unauthorized error.
Existing account with matching address, verified email Existing account with matching address, unverified email **First SSO login, verified IdP token email** ✅ Login with IdP succeeds
🔗 IdP linked to existing account ❌ 401 Unauthorized **First SSO login, unverified IdP token email** ❌ 401 Unauthorized ❌ 401 Unauthorized ## Verifying a changed email address When a user changes their email address using the [current_user/change_email endpoint](https://www.sharetribe.com/api-reference/marketplace.html#change-email-address), a similar verification process takes place for the new address, but using the "Verify changed email address" email notification. The token validity logic and the [endpoint to verify the address](https://www.sharetribe.com/api-reference/marketplace.html#verify-email-address) are the same as for a new sign-up. In addition, Sharetribe sends the "Email address changed" email notification to the old email address about the email address change, if the old address was verified. Before the user verifies the new address, all marketplace notifications will continue their previous delivery pattern - if the previous address was verified, notifications continue to be delivered to that address until the new address is verified - if the previous address was not verified, notifications are not delivered until the new address is verified. --- ## Login as user Path: concepts/users-and-authentication/login-as-user/index.mdx # Login as user The _Login as user_ feature allows marketplace operators to log into their marketplace as a specific user of the marketplace. This helps operators to experience their marketplace as their users do and to find out what is wrong when their users are reporting problems. The feature also comes in handy when a marketplace user asks for help with managing their data and listings. In both Test and Dev environments, you get full access to user profiles and actions with this feature. However, when logged in as a user in a Live environment, it is not possible to modify Stripe account details, send messages, or initiate or transition transactions. Prior to May 22, 2024, using the "Log in as" feature, the operator would be logged in as a marketplace user with limited access rights, regardless of the environment. Now, when logging in as a user in Dev and Test environments, you have full access rights. Live environments remain unaffected, and the "Log in as" feature still only provides limited access rights. From the API perspective, this means that in Dev and Test environments, we grant an authentication token with the scope `user` instead of `user:limited` when using the "Log in as" feature. Consequently, in the marketplace front-end, you can't verify that the current authenticated session has been initiated using the "Log in as" feature by checking if the auth scope is user:limited. Instead, you should use the latest SDK (version 1.21.0) and check the `authInfo.isLoggedInAs` value, as demonstrated in the [LimitedAccessBanner.js file](https://github.com/sharetribe/web-template/blob/b1ab979e45a614005e90f42804d98577ca4675c2/src/components/LimitedAccessBanner/LimitedAccessBanner.js#L29). Versions [v5.1.0](https://github.com/sharetribe/web-template/releases/tag/v5.1.0) and forward support this feature natively. If you're using an older template, the banner saying "You are logged in as ..." will not be displayed when using the "Login as User" feature in either the Dev or Test environments. ## How the Login as user feature works As context, here's a quick description of the technical implementation of how the Login as user works to make it easier to understand the changes it requires. The authentication flow uses the _authorization code_ grant type defined in the OAuth2. Console works as an _authorization server_ that issues an authorization code for Sharetribe Web Template. The template then uses this code to obtain an access token from Auth API. The access token is valid for 30 minutes and it does not come with a refresh token. The token can be used as a normal token obtained with a password login excluding updating payment information, sending messages, and initiating or transitioning transactions. The image below describes the authentication flow in more detail. ![Authentication flow](./authentication-flow.png) Remember to make sure that the `REACT_APP_MARKETPLACE_ROOT_URL` value configured in your marketplace website matches the marketplace URL configured in Console. This value will be used to redirect back to your marketplace, and the value is validated in Console when issuing an authorization code. When developing Sharetribe Web Template locally while testing this feature, you need to set the Marketplace URL as `localhost:4000` and use `yarn run dev-server` so that both your client and server run on the same port. ## Troubleshooting Having trouble enabling the Login as user feature? Check that you have the following in order. ### Authentication fails with message: Failed to authorize as a user Double check that the `REACT_APP_MARKETPLACE_ROOT_URL` environment variable of your marketplace website matches the Marketplace URL you have configured in Console. ### Authentication fails with message: Mismatch between redirect_uri parameter value and Marketplace URL Double check that the `REACT_APP_MARKETPLACE_ROOT_URL` environment variable of your marketplace website matches the Marketplace URL you have configured in Console. ### Authentication fails with message: Unable to authenticate as a user Have you updated the SDK to the latest version? ### Login session drops unexpectedly The access token obtained with the Login as user authentication flow is valid only for 30 minutes. If you could not finish what you had in mind during that time you can always login as the user again. --- ## Referral links in Sharetribe Path: concepts/users-and-authentication/referral-links/index.mdx # Referral links in Sharetribe Referral links allow operators to track how new users arrive at the marketplace. When a user follows a referral link, the referral parameters in the URL are validated against the referral sources configured for each user type in Console. If the user then signs up before the expiry window, the referral data is saved to their private data. ## Configuring referrals in Console Referral sources are configured per user type in Console, so a referral link is only valid for users who match the expected user type on sign-up. ![Console referral link example](./console-referral-link-example.png) For more information on setting up referrals, refer to the [Help Center article](https://www.sharetribe.com/help/en/articles/15478987-how-referral-sources-work). ## How referral links work Referral data is captured from the URL on any page load, meaning any page on the marketplace can serve as an entry point for a referral link. In pages where the URL directly impacts API query parameters - for example the search page - the referral link is filtered out of the params before the query is made. A referral link uses the URL parameter name defined in Console combined with a referral code. For example, a referral link for a cycling club partnership might look like this: ``` /signup?clubReferral=referral-code ``` If your custom search code passes URL parameters directly to API queries - for example, by spreading `location.search` params into a listings query - referral link parameters may be unintentionally included. Filter them out before making API calls. See [SearchPage.duck.js](https://github.com/sharetribe/web-template/blob/main/src/containers/SearchPage/SearchPage.duck.js) for an example of how the template handles this. ### Persisting referral data When referral parameters are found in the URL, they are saved to local storage as referral data. The referral data persists for 90 days, so a user can follow a referral link and sign up at a later point within that window and still have the referral captured. ### Validation and sign-up When a user signs up, the referral data stored in local storage is validated against the referral sources defined for each user type in Console. The referral sources for a user type are structured as follows: ```json { "userType": "seller", "referralSources": [ { "label": "Cycling club partnership", "parameter": "clubReferral" } ] } ``` If the referral data is valid and the user matches the expected user type, the referral data is saved to the user's private data. If the referral data is invalid, or the user's type does not match the referral source, sign-up proceeds normally and no referral data is saved. --- ## Social logins & SSO Path: concepts/users-and-authentication/social-logins-and-sso/index.mdx # Social logins & SSO This document gives an overview of how different login solutions work with Sharetribe. To find guidance on how to implement login using a specific service, refer to the following how-to guides: - [Enable Facebook login](/how-to/users-and-authentication/enable-facebook-login/) - [Enable Google login](/how-to/users-and-authentication/enable-google-login/) - [Enable OpenID Connect login](/how-to/users-and-authentication/enable-open-id-connect-login/) - [How to set up OpenID Connect proxy in Sharetribe Web Template](/how-to/users-and-authentication/setup-open-id-connect-proxy/) ## How a third party identity provider authentication works In addition to username and password based authentication, Sharetribe allows marketplace users to authenticate using a third party identity provider. An identity provider can be used to authenticate the user - when a new user account is created - or when a user logs into the marketplace to a previously created account If the user is logging in to an existing account, it is not necessary that the account was originally created using the identity provider. If the Sharetribe user has a **verified email address** that matches the **verified email address** in the identity provider token, the user can authenticate to the matching account with the identity provider. A general overview of using a third party identity provider when logging in or creating a user is as follows: ![Auth flow using a 3rd party identity provider](./auth-flow.png 'Auth flow using a 3rd party identity provider') The different actors in the diagram above are: - **Browser** The Sharetribe Web Template React application running in user's browser - **Template backend** Sharetribe Web Template Node application that runs on a server - **Identity provider** A service that provides user authentication, for example, Facebook - **Sharetribe API** Sharetribe Marketplace or Auth API Steps 1-4 describe a standard OAuth2/OpenID Connect login flow. The details may differ depending on the identity provider that is being used. ### Initiate authentication in the marketplace The Sharetribe Web Template client calls the Sharetribe Web Template browser to initiate the SSO authentication, and initiates a redirect to the identity provider's site. ### Authenticate user with the identity provider The user takes the necessary steps to authencate with the identity provider. After a successful authentication, an authorization code is returned from the identity provider. ### Call the redirect_uri on the server The `redirect_uri` you define in your identity provider should have a corresponding endpoint on your server. For the built-in Google and Facebook SSOs, this endpoint exists by default in the Sharetribe Web Template. For any custom SSO implementations, you need to create this endpoint. ### Exchange the authorization code for a token with the identity provider From the endpoint in `redirect_uri`, the server sends a request to the identity provider to trade the authorization code for a token. The token that is obtained depends on the identity provider and protocol in use. Read more about the types of tokens expected by each supported identity provider: - [Authentication API reference – Identity providers](https://www.sharetribe.com/api-reference/authentication.html#identity-providers) ### Call the Sharetribe API with the token **5.1** Invokes `/current_user/create_with_idp` endpoint in Sharetribe Marketplace API. The token obtained from steps 1.-4. is passed here among a few other details. Returns a current user entity. **5.2** Invokes `/auth_with_idp` endpoint in Sharetribe Auth API. The token obtained from steps 1.-4. is passed here among a few other details. Returns access and refresh tokens. Step 5.1 does not yet authenticate the user to the marketplace. If you create a user with an identity provider token, you need to use that same token to then immediately authenticate the newly created user to the marketplace. ### Validate the token The Sharetribe backend validates the token passed in as a parameter in 5.1 or 5.2. Depending on the identity provider in use, this may or may not include a request to the identity provider. After these steps are successful, the user has the necessary authentication to use the marketplace. --- ## User access control Path: concepts/users-and-authentication/user-access-control-in-sharetribe/index.mdx # User access control By default, all users in a Sharetribe marketplace have the same permissions to join, post listings, and start transactions. However, you can modify some of these permissions in Console, under the "Access control" tab. ![Access control options](./access_control_blank.png) Access control features can be toggled on or off on the marketplace level, and some permissions can also be modified per user. Certain access control settings may also enforce authorization in certain API endpoints. When an operator makes a user-level change to a single user's permissions, it triggers a _user/updated_ event that you can listen to with Integration API. However, when an operator toggles a feature on or off on the marketplace level, no event is triggered, even though the effective permissions may change for some users. ## Make marketplace private By default, Sharetribe marketplaces are public. This means that listings and user profiles are visible to unauthenticated users. By making your marketplace private, you allow only authenticated users to view listings and users on your marketplace. On a private marketplace, the only public Marketplace API endpoints are user creation and password reset related. All other endpoints require an authenticated user access token. This setting can be turned on or off on the marketplace level. See which endpoints are affected by this setting [here](#making-marketplace-private). Read more about this feature in the [Help Center](https://www.sharetribe.com/help/en/articles/9503164-make-marketplace-private). ## Disable file uploads and downloads By default, Sharetribe marketplaces allow uploading and downloading files and using them as file attachments on other resources. You can also disable uploading and downloading files on a marketplace level. ## Approve users who want to join User approval means that when a user signs up, they need to be approved by an admin before they have full access to the marketplace. On **public marketplaces**, this means that users pending approval can view listings and other users' profiles, but they cannot post listings or initiate transactions. On **private marketplaces**, users pending approval can only view their own profile – they cannot view any other marketplace data. You can enable this setting in the Access Control tab. See which endpoints are affected by this setting [here](#approve-users-who-want-to-join-1). Read more about this feature in the [Help Center](https://www.sharetribe.com/help/en/articles/9503152-approve-users-who-want-to-join). ## Restrict listing posting rights You might also want to limit listing posting rights to certain users only. For example, if you have user types "Buyer" and "Seller", you might want to grant listing posting rights to sellers only. Or you might be monetizing your marketplace with subscriptions, and therefore you only want to grant posting rights to users who have subscribed. On the marketplace level, you can toggle the selection in the Access control tab. See which endpoints are affected by this setting [here](#restrict-posting-rights). Read more about this feature in the [Help Center](https://www.sharetribe.com/help/en/articles/9503118-restrict-listing-publishing-rights). ## Restrict transaction rights You might also want to restrict the ability to initiate transactions to specific users only. For example, if you have a marketplace where users can view listings but need to verify their identity before making purchases, you can enable this feature to manually control who can initiate transactions. Similarly, if your platform uses a subscription model, you may want to allow only paying users to start transactions. In some cases, you may run a marketplace that functions primarily as a showroom, where users can browse but cannot buy until the operator grants them permission. You can toggle the setting in the Access control tab. Once this checkbox is selected, similarly to how publishing rights are granted, you can see the permission status of each user in the Console's Manage > Users view. See which endpoints are affected by this setting [here](#restrict-transaction-rights-1). Read more about this feature in the [Help Center](https://www.sharetribe.com/help/en/articles/9790336). ## Restrict viewing rights On private marketplaces, you can restrict viewing rights for individual users. If a user's viewing rights have been restricted, they will only see their own listings and profile even if they have previously been approved. You can toggle the setting in the Access control tab when the marketplace has been set to private. Once this checkbox is selected, you can see the permission status of each user in the Console's Manage > Users view. See which endpoints are affected by this setting [here](#restrict-viewing-rights-1). Read more about this feature in the [Help Center](https://www.sharetribe.com/help/en/articles/9790341). ## API level restrictions Toggling permissions under the "Access Control" tab in the Console directly impacts API behavior. When permissions are modified, endpoint access is adjusted accordingly, ensuring that users cannot bypass the restrictions imposed by these settings. This guarantees access control policies are strictly enforced. These restrictions only apply in the Marketplace API. Marketplace end-users never access the Integration API directly, so Integration API endpoints don't need to be similarly restricted. However, if you create an integration where marketplace users indirectly utilize the Integration API (e.g. proxying calls via the server), you'll need to enforce restrictions based on the currentUser's effectivePermissionSet. This might also be relevant if you have more complex customizations in place that utilize the Integration API, e.g., you're allowing users to post listings on behalf of other users using the Integration API. ### Making marketplace private Toggling this setting in Console requires users to log in to access listings. Making the marketplace private from Console will restrict the following endpoints: - `GET /users/show` - `GET /listings/query` - `GET /listings/show` - `GET /timeslots/query` - `GET /reviews/query` - `GET /reviews/show` - `GET /sitemap_data/query_listings` When this setting is toggled, these endpoints will return a 403 Forbidden response, indicating that access is denied due to the marketplace's private status. ### Disabling file uploads and downloads Toggling this setting on prevents users from uploading and downloading files to and from Sharetribe storage. This selection will restrict the following endpoints: - `POST /file_uploads/create` - `POST /file_downloads/create` In other words, endpoints showing file or file attachment information are not restricted, only endpoints that create signed URLs to Sharetribe file storage to upload or download a file entity. ### Approve users who want to join Toggling this setting in Console will require users to be approved by an operator to join the marketplace. This will restrict the following endpoints: Listings: - `POST /own_listings/create` - `POST /own_listings/create_draft` - `POST /own_listings/publish_draft` - `POST /own_listings/discard_draft` - `POST /own_listings/open` - `POST /own_listings/close` - `POST /own_listings/update` - `POST /own_listings/add_image` Transactions: - `POST /transactions/initiate` - `POST /transactions/initiate_speculative` - `POST /transactions/transition` - `POST /transactions/transition_speculative` Availability exceptions: - `POST /availability_exceptions/create` - `POST /availability_exceptions/delete` Stock adjustments: - `POST /stock_adjustments/create` - `POST /stock_adjustments/compare_and_set` When this setting is toggled, these endpoints will return a 403 Forbidden response, indicating that access is denied because the user has not been approved. Note that if the marketplace has been set to private, unapproved users will be treated the same as non-registered users. See the endpoints that are restricted in this case [above](#approve-users-who-want-to-join-1). ### Restrict posting rights Toggling this setting in the Console allows you to manually select which users can post listings. - `POST /own_listings/create_draft` - `POST /own_listings/publish_draft` - `POST /own_listings/create` - `POST /own_listings/open` These endpoints will return a 403 Forbidden response if the user does not have post rights. ### Restrict transaction rights Toggling this setting in Console allows you to manually select which users can initiate transactions. When transaction rights have been revoked for a marketplace user, the following Marketplace API endpoint will be restricted: - `POST transactions/initiate` This endpoint will return a 403 Forbidden response if the user doesn't have permission to initiate transactions. Note that users can still call the `transactions/transition` endpoint, allowing them to transition existing transactions. This can be restricted in the client application; for example, the web-template prevents users from transitioning existing transactions that are in the inquiry state. ### Restrict viewing rights Toggling this setting in Console allows you to manually select which users can view other users' listings and profiles on your private marketplace. When viewing rights have been revoked for a marketplace user, the following Marketplace API endpoints will be restricted: Listings: - `GET listings/query` - `GET listings/show` Reviews: - `GET reviews/query` - `GET reviews/show` Timeslots: - `GET timeslots/query` ## Permissions in the access-control.json asset On the marketplace level, the changes made in Console get recorded in the asset _/general/access-control.json_. ```json { "id": "66cc804f-b5ee-44f2-8c7e-ca3e950e534f", "type": "jsonAsset", "attributes": { "assetPath": "/general/access-control.json", "data": { "marketplace": { "private": false }, "users": { "requireApprovalToJoin": false, "requireApprovalToJoinOptions": { "callToAction": { "type": "internal", "text": "Add a link to request approval to join", "href": "/p/about/" } }, "requirePermissionToPostListings": true, "requirePermissionToPostListingsOptions": { "callToAction": { "type": "none" } }, "requirePermissionToInitiateTransactions": false, "requirePermissionToInitiateTransactionsOptions": { "callToAction": { "type": "none" } }, "requirePermissionToRead": false, "requirePermissionToReadOptions": { "callToAction": { "type": "none" } } }, "listings": { "requireApprovalToPublish": false, "requireApprovalToPublishOptions": { "callToAction": { "type": "none" } } } } } } ``` In addition to permission data, the asset contains an options object, which contains data on a Call To Action button. This feature is implemented in the template, see the relevant [PR here](https://github.com/sharetribe/web-template/releases/tag/v5.6.0). ## Permissions in the currentUser resource Permissions show up in the _currentUser_ resource in two ways: - _currentUser_ has an attribute _permissions_, which contains the user-level permission setting ```json "~:type": "~:currentUser", "~:attributes": { "~:deleted": false, "~:banned": false, "~:email": "pending-approval@example.com", "~:permissions": { "~:read": "~:permission/allow", "~:initiateTransactions": "~:permission/deny", "~:postListings": "~:permission/deny" }, "~:stripeConnected": false, "~:stripePayoutsEnabled": false, ``` - _currentUser_ also has a related resource _effectivePermissionSet_, which contains the user's permissions based on the user level and marketplace level settings. You will need to explicitly [include this related resource](https://www.sharetribe.com/api-reference/#including-related-resources) in your _currentUser.show()_ API call to fetch it from the API. ```json "~:relationships": { "~:effectivePermissionSet": { "~:data": { "~:id": "~u6707a063-994a-4310-92c8-422831800720", "~:type": "~:permissionSet" } } }, }, "~:included": [ { "~:id": "~u6707a063-994a-4310-92c8-422831800720", "~:type": "~:permissionSet", "~:attributes": { "~:postListings": "~:permission/deny", "~:read": "~:permission/allow", "~:initiateTransactions": "~:permission/deny" } } ] ``` This is an important distinction, because the _currentUser.attributes.permissions_ value might be different from the _effectivePermissionSet_ value. For this reason, you should always use the _effectivePermissionSet_ value to determine the user's access in custom code, because it takes into account both user-level and marketplace-level permissions. Consider this example: - A marketplace has enforced manual permission to publish listings - User A has had their publishing rights revoked. Both their _attributes.permission_ value and their _effectivePermissionSet_ value for _postListings_ are _"permission/deny"_. - Marketplace operator has a campaign where they want to grant all users posting rights for 24 hours, and they deselect the checkbox for requiring manual permission to publish listings. Now, the _attributes.permission_ value for _postListings_ is still _"permission/deny"_, because it persists on the user's profile. ```json "~:type": "~:currentUser", "~:attributes": { "~:deleted": false, "~:banned": false, "~:email": "pending-approval@example.com", "~:permissions": { "~:read": "~:permission/allow", "~:initiateTransactions": "~:permission/deny", "~:postListings": "~:permission/deny" }, "~:stripeConnected": false, "~:stripePayoutsEnabled": false, ``` The _effectivePermissionSet_ value for _postListings_, however, is _"permission/allow"_, because now the marketplace level restriction has been lifted. The marketplace level setting overrides the user level setting. ```json "~:relationships": { "~:effectivePermissionSet": { "~:data": { "~:id": "~u6707a063-994a-4310-92c8-422831800720", "~:type": "~:permissionSet" } } }, }, "~:included": [ { "~:id": "~u6707a063-994a-4310-92c8-422831800720", "~:type": "~:permissionSet", "~:attributes": { "~:postListings": "~:permission/allow", "~:read": "~:permission/allow", "~:initiateTransactions": "~:permission/deny" } } ] ``` --- ## Users and authentication in Sharetribe Path: concepts/users-and-authentication/users-and-authentication-in-sharetribe/index.mdx # Users and authentication in Sharetribe Anyone who registers to your Sharetribe marketplace is referred to as a user. In addition, the operator can take certain actions on the marketplace even though they are not technically a user there. Sharetribe marketplaces only facilitate transactions between registered users – in other words, it is not possible for someone to purchase or book a listing without signing up as a user. ## User roles in Sharetribe Sharetribe has two possible roles for a registered user: customer and provider. All users can be both customers and providers by default. This means that all users by default can be the party making a payment in a transaction, i.e. a customer, as well as the party receiving a payment in another transaction, i.e. a provider. If you want to limit certain users to only customers or only providers, you will need to create those limitations in your client application. You can use [extended data](/concepts/extended-data/extended-data-introduction/) to determine that a user is in a certain group, and then allow a subset of your marketplace functionalities, e.g. listing creation, for a specified group. You can, for example, [define user types in Console](https://www.sharetribe.com/help/en/articles/9117175-what-are-user-types) for this purpose. ### Customer In a transaction, a **customer** is the user who purchases or books the product or service. In a regular marketplace flow, the customer is the user who starts a transaction against a provider's listing, and in a reverse marketplace flow, the customer is the user who creates the listing. In marketplaces with payments, the customer is the user who pays the listing price. Customers can save their payment details in Sharetribe. Customers need to enter a valid email address to sign up to Sharetribe, but other than that customers are not required to enter further information to use the marketplace. When using the Stripe default payment integration, customers can save a payment method, however the payment method information is saved in Stripe and not directly in Sharetribe. Within a transaction, customers can initiate or transition transactions specified for the customer. ### Provider When a user provides a product or service and someone else books or purchases that offering, the user is considered the **provider** of the transaction in question. In a regular marketplace flow, the provider is the user who creates the listing, and in a reverse marketplace flow, the provider is the user who starts a transaction against a customer's listing. In marketplaces with payments, the provider is the transaction party who receives the transaction payment. This means that if the payment happens within the marketplace, the provider will need to verify their identity to the payment gateway to adhere to the payment processor's [Know Your Customer requirements](https://en.wikipedia.org/wiki/Know_your_customer). Within a transaction, providers can initiate or transition transactions with transitions specified for the provider. ### Transaction related roles: operator and system A **marketplace operator** is not a user in the marketplace – they cannot sign in to the marketplace with the same credentials they use to sign in to Sharetribe Console. The operator can, however, take actions on the marketplace through Sharetribe Console or Integration API, when those actions are defined for the operator. Operators cannot participate in the messaging between customer and provider within the transaction. A transaction transition is performed by the **system** if it is scheduled to happen automatically. ## User access Sharetribe marketplace listings and user profiles on public marketplaces can be viewed by anyone by default, whether they are registered users or not. If you set your marketplace to private, only registered users can view listings and user profiles. When a user wants to start a transaction or create a listing, they need to be a registered user on your marketplace. You can also choose to limit additional rights for individual registered users. ### Registered marketplace users Listings can only be updated by their author, i.e. the registered user who originally created the listing. Operators can create listings for a registered user, and update existing listings. When user approval is required on the marketplace, operators can individually determine which users can post listings and start transactions. On private marketplaces, user approval affects viewing rights as well. Listing posting rights can also be determined per user. Beyond that, Sharetribe does not have different levels of user access within the marketplace. Operators who want to create more complex user hierarchies will need to think about the levels of user access they want each custom role to have, and potentially use a custom backend solution to complement Sharetribe default user management. If you are contemplating creating a user hierarchy in your Sharetribe marketplace, contact [Sharetribe Support](mailto:hello@sharetribe.com) and let us know your use case – we're happy to help you figure out a suitable solution! ### Login as user Sharetribe has a feature through which operators can log in to their marketplace as a registered user and take limited actions on their behalf. When using the Login as user feature in Live environments, operators cannot initiate or transition transactions or modify the user's payout information. However, they can e.g. create and update listings on behalf of the user. The login as user feature can be accessed through the Sharetribe Console, by navigating to a user profile and clicking on the three dots next to the profile image of the user. ### Integration API Sharetribe Integration API allows trusted secure applications to access all data within a marketplace. It is not accessible for marketplace users with their own sign-in credentials. Instead, Integration API can be used to create server-side integrations to external systems, or to retrieve data for custom marketplace dashboards. ### Authenticating to Sharetribe APIs Sharetribe marketplace users need to sign up with their email address to create listings and participate in transactions. Alternatively, they can use [social logins](/concepts/users-and-authentication/social-logins-and-sso/) to sign up, or to login with an email address that already has a user within Sharetribe. Sharetribe has a separate [Authentication API](/concepts/api-sdk/authentication-api/) that handles authentication to other Sharetribe APIs. Both Marketplace API and Integration API require valid access tokens to be passed in every API request. If you use the [Javascript SDKs](/concepts/api-sdk/js-sdk/) in your marketplace client application, they handle authenticating the user automatically when they enter their credentials. ## Restricted user states in a Sharetribe marketplace When a user is pending approval, banned, or deleted, they do not have full access to the marketplace functionalities. User state is exposed as a part of the `currentUser` resource. ### User pending approval You can set your marketplace to require that users be approved before they can participate in the marketplace. You can approve users in Console. For public marketplaces, this means that users cannot post or modify listings or start transactions before they have been approved. For private marketplaces, users pending approval also cannot view listings or other user profiles. A user pending approval can edit their own profile, but it will only become visible to other marketplace users once the user has been approved. ### Banned user Banning a user means removing the user and all of the user’s listings from a marketplace due to inappropriate behaviour. The email with which a banned user registered to the marketplace can not be used to create new accounts. The user data is only visible when it is linked to, and even then only ID and banned status are shown. Operators can ban and unban users through Sharetribe Console, but there is no endpoint in the Sharetribe APIs to ban a user. Unbanning a user does not automatically reinstate the user's deleted listings. ### Deleted user Deleting a user means completely and irreversibly removing all of the user's personal data. This includes all of the public-facing data like profile and listings as well as the user account information. Operators can delete users through Sharetribe Console. In addition, there is an endpoint in Marketplace API so that operators can build a functionality for users to delete their own accounts. We have a how-to guide on [implementing a _Delete user_ feature](/how-to/users-and-authentication/implement-delete-user/). ## Authentication in Sharetribe The Sharetribe APIs limit visibility to certain data based on the authentication level of the user. Marketplace API has multiple levels of access, whereas Integration API only has full access or no access. This means that when using any Integration API endpoints, it is crucial to only use them from a secure context i.e. from server code, never from browser code. Regardless of the level of access, each API endpoint requires an access token that can be acquired through Sharetribe [Authentication API](https://www.sharetribe.com/api-reference/authentication.html). When using the [Sharetribe Javascript SDKs](/concepts/api-sdk/js-sdk/), authentication is handled with [specific SDK methods for Marketplace API](https://sharetribe.github.io/flex-sdk-js/authentication.html) and [upon instantiation in Integration API](https://sharetribe.github.io/flex-integration-sdk-js/authentication.html). ### Anonymous access to Marketplace API Some endpoints can be accessed without signing in to Sharetribe on public marketplaces. These include viewing published listings, availability and reviews, as well as public user data. In addition, the user creation endpoints and password reset request endpoint can be called with an anonymous access token. Password reset endpoint requires a `passwordResetToken` that is sent as a response to the password reset request command, and the token is sent directly to the email specified in the request. ### User access to Marketplace API Only authenticated users can access endpoints that deal with updating user information, creating and updating listings, and initiating and transitioning transactions. On private marketplaces, all endpoints require an authenticated user access token, and operators can also further limit individual users from posting or editing listings, or from viewing marketplace data. Through initiating and transitioning transactions, authenticated users have access to functionalities that do not have specific endpoints. For instance creating and accepting bookings and reviewing transaction counterparties are actions that can only happen within the context of a transaction process. ### Trusted access to Marketplace API Some transitions within a transaction process can include privileged actions that require a trusted context i.e. they are [privileged transitions](/concepts/transactions/privileged-transitions/). Privileged actions include [setting the transaction line items](/references/transaction-process-actions/#actionprivileged-set-line-items) and [updating the transaction metadata](/references/transaction-process-actions/#actionprivileged-update-metadata). These transitions require a trusted token or a trusted SDK method, both of which are obtained using the Sharetribe application client secret. In practice, the trusted context is a server environment. With the Sharetribe Web Template, the client application server has default implementations of trusted endpoints for [initiating](https://github.com/sharetribe/web-template/blob/main/server/api/initiate-privileged.js) and [transitioning](https://github.com/sharetribe/web-template/blob/main/server/api/transition-privileged.js) transactions. ### Full access to Integration API The Integration API offers access to the entire marketplace data. This includes all users, listings, transactions, and messages. To see what endpoints you can access using the Integration API, refer to the [Integration API reference](https://www.sharetribe.com/api-reference/integration.html). To access the Integration API you need a valid access token obtained through [the Authentication API](/concepts/api-sdk/authentication-api/#authentication-api) or the [Sharetribe Integration SDK](https://sharetribe.github.io/flex-integration-sdk-js/authentication.html). You should only grant access to trusted applications, such as ones that run in your own backend systems, or applications that can only be executed by authorized marketplace operators. In order to gain authorisation, you need to authenticate using the client ID and client secret of your Integration API application. Read more on how to authenticate in the [Authentication API reference](https://www.sharetribe.com/api-reference/authentication.html). --- ## How to customise a Section, Block or Field using options Path: how-to/content-management/options-prop/index.mdx # How to customise a Section, Block or Field using options ## Introduction The Sharetribe Web Template renders content pages using data from Pages, the Sharetribe headless content management system. This how-to article assumes a basic understanding of the Pages feature and how the template renders content pages using the PageBuilder. We suggest reading the following articles before proceeding: - [Content management in Sharetribe](/concepts/content-management/content-management-in-sharetribe/) - [Assets reference](/references/assets/) - [How the template renders content pages](/template/content-management/page-builder/) This guide will introduce you to best practices for styling and customising components used by the PageBuilder. ## Customising components Sharetribe Web Template uses a component called the PageBuilder to render content pages. You will likely want to change the style and structure of content pages, and the PageBuilder is where you should make those changes. It is good to note that changes to the PageBuilder will affect how **all content pages** are rendered. If you only want to customise a specific section on an individual content page, you need to take a different approach. Let’s lay out a more concrete example of this scenario: say you want to define that for all Sections on the landing page using the template section-article, the title and ingress should be aligned left, and on all other pages they should adhere to the default styling, i.e. be centred: ![e](./example1.png) Making these changes directly in the SectionArticle component will result in changes across all content pages. Let’s demonstrate. We will create a new rule set in SectionArticle.module.css that does not apply any centering: ```css filename="SectionArticle.module.css" .title { max-width: 30ch; } .ingress { max-width: 65ch; } ``` Next, let’s use these rulesets in the SectionArticle.js component: ```diff filename="SectionArticle.js" {hasHeaderFields ? (
- + - + ``` We’ll now see that the rule set is applied to all Sections using the section-article template, on all content pages: ![Two content pages with content aligned left](./left-aligned.png) However, our goal is to be able to apply the styling to a single content page. Let’s see how that happens next. ## Using the options prop The best way to accomplish this is by using the _options_ prop to override the section on a specific page. Options is a prop that can be passed to the _pageBuilder_ component. The prop takes an object used to map either a **Section**, **Block** or **Field** component to a custom component. Let's first create a new component based on the original **SectionArticle** component. The easiest way to do this is to duplicate the **SectionArticle** directory. Let’s rename the duplicate **SectionArticleAlignLeft**: ![Screenshot depicting new file](./vscode-sc.png) Remember to also rename all files within the new directory from **SectionArticle** to **SectionArticleAlignLeft**: ![Screenshot depicting new file](./vscode-sc2.png) We can now reapply the changes we made in the previous chapter to our new component. First, create a new rule set in SectionArticleAlignLeft.module.css: ```css filename="SectionArticleAlignLeft.module.css" .title { max-width: 30ch; } .ingress { max-width: 65ch; } ``` And change the SectionArticleAlignLeft.js file to use the new rule set we just created: ```diff filename="SectionArticleAlignLeft.js" {hasHeaderFields ? (
- + - + ``` To see this change live, we will have to import the new component and use the _options_ prop in _LandingPage.js_: ```js filename="LandingPage.js" ``` We can then define an object in LandingPage.js that will be passed on as the _options_ prop: ```js filename="LandingPage.js" const sectionOverrides = { article: { component: SectionArticleAlignLeft }, }; ``` Let's pass that object to the **PageBuilder** component in LandingPage.js as the _options_ prop: ```diff filename="LandingPage.js" return ( } featuredListings={getFeaturedListingsProps(camelize(ASSET_NAME), props)} + options={{sectionComponents: sectionOverrides}} /> ``` When we go to the landing page, we can see that the the template now uses the new component we created to render the section article. This component will only render section articles on the landing page. That is evident when we navigate to any other content page: we will see that titles and content are centred. ## Overriding Blocks and Fields Similar overrides can be made for Blocks and Fields too. For example, if we had wanted to override a Block instead of a Section, we could have used the following mapping: ```js const blockOverrides = { ['defaultBlock']: { component: CustomBlock }, }; ``` And passed it as an options prop: ```js options={{blockComponents: blockOverrides}} ``` Field overrides can be made similarly. Overriding the **H1** tag would be defined using the following structure: ```js const fieldOverrides = { heading1: { component: CustomH1, pickValidProps: exposeContentAsChildren, }, }; ``` Overriding a Field requires passing a function to _pickValidProps_ to ensure data validation. You can import the _exposeContentAsChildren_ function from **Field.helpers**: ```js ``` In this case, we would define our custom heading component in the Heading.js file: ```js filename="Heading.js" export const CustomH1 = React.forwardRef((props, ref) => { const { rootClassName: rootClass, as, ...otherProps } = props; return ( ); }); CustomH1.displayName = 'CustomH1'; ``` And export it in the PageBuilder/Primitives/Heading/index.js file: ```js filename="PageBuilder/Primitives/Heading/index.js" export { CustomH1, H1, H2, H3, H4, H5, H6 } from './Heading'; ``` Finally, we’d pass it on to the _options_ prop in LandingPage.js: ```js filename="LandingPage.js" options={{fieldComponents: fieldOverrides}} ``` For a list of which Fields can be overridden, see [Field.js](https://github.com/sharetribe/web-template/blob/main/src/containers/PageBuilder/Field/Field.js). --- ## Edit email templates with Sharetribe CLI Path: how-to/emails-and-notifications/edit-email-templates-with-sharetribe-cli/index.mdx # Edit email templates with Sharetribe CLI Sharetribe CLI (Command-line interface) is a tool for changing your marketplace's advanced configurations such as transaction processes and email templates. This guide expects that you have already installed Sharetribe CLI and are logged in with your API key. If not, it's recommended to first read the guide [Getting started with Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/). We also recommend that you go through the [Edit transaction process with Sharetribe CLI](/how-to/transaction-process/edit-transaction-process-with-sharetribe-cli/) tutorial to understand process pulling, editing, pushing, and alias handling on a general level. In this tutorial we make a change to an email template that is used in sending notifications to your marketplace users as part of your transaction process. These transaction email template changes are also a form of process change, and they create a new version of your process. For email notifications not part of your transaction process, see the [Built-in email notifications](https://console.sharetribe.com/advanced/email-notifications) page in the Build > Advanced > Email notifications section of Console. For content-only changes, you can use the Console transaction email editor in Build > Content > Email texts. ## Pull existing process To edit the transaction email templates, you need to pull an existing process with its templates. First, let's list all the processes of our `my-marketplace-dev` marketplace: ```bash flex-cli process list -m my-marketplace-dev ``` The `process list` command prints out all the processes and their latest versions. You want to pick the correct process and version from this list. In this tutorial we will use the `default-booking` process, version 1. You probably have different transaction processes in your marketplace - so, you need to adjust this guide accordingly. Let's pull that process version: ```bash flex-cli process pull --process default-booking --version 1 --path process -m my-marketplace-dev ``` This will create a `process/` directory that has all the process files in it: - `process.edn` file, which describes the transaction process - `templates` directory, which contains all the transaction email templates for this process ## Templates directory Let's see what we have in the `process/` directory: ![Process directory contents](./process-dir.png) If you look at the `:notifications` key in the `process.edn` file, you will see that the template directories and file names match the `:template` values in the notifications: ```clojure :notifications [{:name :notification/booking-new-request, :on :transition/confirm-payment, :to :actor.role/provider, :template :booking-new-request} {:name :notification/booking-accepted-request, :on :transition/accept, :to :actor.role/customer, :template :booking-accepted-request} {:name :notification/booking-operator-accepted-request-to-customer, :on :transition/operator-accept, :to :actor.role/customer, :template :booking-accepted-request} ;;... ``` A template for a notification is a directory that is named after the `:template` value and contains two files: - `TEMPLATE_NAME-subject.txt` - holds the mail Subject line template - `TEMPLATE_NAME-html.html` - contains the template for the HTML version of the mail Both parts are mandatory. All emails that are sent from the marketplace contain both the HTML and plain text variants and the recipient's mail client is free to choose which one to visualize and how. The text version is automatically generated from the HTML template. ### Example For example, the `:notification/booking-new-request` notification: ```clojure {:name :notification/booking-new-request, :on :transition/confirm-payment, :to :actor.role/provider, :template :booking-new-request} ``` has the following template: ``` booking-new-request ├── booking-new-request-html.html └── booking-new-request-subject.txt ``` Note that the template name (e.g. `:booking-new-request`) doesn't have to match the notification name (e.g. `:notification/booking-new-request`) as you can use the same template in multiple notifications. ## Email template syntax The templates use [Handlebars](http://handlebarsjs.com/) syntax. Example HTML: ```handlebars {{set-translations (asset "content/email-texts.json")}} {{set-locale (asset "general/localization.json" "locale" "en_US")}} {{set-timezone transaction.listing.availability-plan.timezone}} {{~#*inline "format-money"~}}{{format-text "{amount,number,::.00} {currency}" amount=money.amount currency=money.currency}}{{~/inline~}} {{~#*inline "format-day"~}}{{#with transaction.listing.availability-plan}}{{format-text "{date,date,::EE}" date=date}}{{/with}}{{~/inline~}} {{~#*inline "format-day-before"~}}{{#with transaction.listing.availability-plan}}{{format-text "{date,date,::EE}" date=(date-transform date days=-1)}}{{/with}}{{~/inline~}} {{~#*inline "format-day-time"~}}{{#with transaction.listing.availability-plan}}{{format-text "{date,date,::EEhmma}" date=date}}{{/with}}{{~/inline~}} {{~#*inline "format-month-date"~}}{{#with transaction.listing.availability-plan}}{{format-text "{date,date,::MMMd}" date=date}}{{/with}}{{~/inline~}} {{~#*inline "format-month-date-day-before"~}}{{#with transaction.listing.availability-plan}}{{format-text "{date,date,::MMMd}" date=(date-transform date days=-1)}}{{/with}}{{~/inline~}} {{#with transaction}}

{{t "BookingNewRequest.Title" "You have a new booking request for {listingTitle}" listingTitle=listing.title}}

{{t "BookingNewRequest.Description" "{customerDisplayName} requested to book {listingTitle} in {marketplaceName}." customerDisplayName=customer.display-name listingTitle=listing.title marketplaceName=marketplace.name}}

{{t "BookingNewRequest.AcceptText" "You need to accept the request by {date,date,::hmmaYYYYMMMd}. Otherwise the request will expire automatically and you won't get paid." date=delayed-transition.run-at}}

{{t "BookingNewRequest.DeclineOptionText" "If the booked dates don't work for you, you can also choose to decline the request."}}

{{t "BookingNewRequest.AcceptOrDeclineLink" "Accept or Decline the booking"}}

{{t "TransactionEmails.AccessibleLinkText" "Can’t click the button? Here’s the link for your convenience:"}} {{marketplace.url}}/sale/{{url-encode id}}/

{{/with}}

{{t "TransactionEmails.MembershipParagraph" "You have received this email notification because you are a member of {marketplaceName}. If you no longer wish to receive these emails, please contact {marketplaceName} team." marketplaceName=marketplace.name}}

``` Variables within `{{ }}` are expanded and escaped, so that they are safe to place inside HTML content. As seen above, some variables have nested values, which can be accessed with dot `.` operator. In the example above, the `#with` block helper is used to access properties of the email context top level property `transaction`. So `customer.display-name` within the `{{#with transaction}}` block will refer to the value of `transaction.customer.display-name` in the email context. The template syntax supports conditionals, loops, helpers and other constructs. For details on the Handlebars constructs and a full description of the email context, see the [Email templates](/references/email-templates/) reference. ## Make a change You can edit every piece of text or punctuation used in your marketplace's email notifications using the Console. Read more in the help centre on [How to edit email texts](https://www.sharetribe.com/help/en/articles/8459261-how-to-edit-email-texts). Most changes to the email templates such as the color of the buttons or the copy text can be modified using no-code tools. For more significant modifications, you will need to edit the code of the email templates directly. Make any change to the `booking-new-request/booking-new-request-html.html` file in a text editor. This could be updating text, styling, or structure. The specific change doesn't matter for this demonstration. When you've made the change, save the file. ## Preview your changes You can test your changes and templates by previewing or sending a test email using your local template files. To preview the change above: ```bash flex-cli notifications preview --template process/templates/booking-new-request -m my-marketplace-dev ``` The command will render the given template using [a sample data (context)](#sample-email-context) and open a browser tab with the output HTML: ![Email preview in a browser](./preview-browser.png) It will also output the rendered text version to the terminal: {/* ![Email preview in terminal](./preview-terminal.png) */} ```bash Preview fetched Template: booking-new-request Subject: Jane P requested to book Wooden sauna Plain text content: You have a new booking request for Wooden sauna Jane P requested to book Wooden sauna in Bikesoil. Start End Fri Tue 1 Dec 12 Dec USD 300.00 × 3 nights 900.00 USD Bikesoil fee -90.00 USD You'll earn 810.00 USD You need to accept the request by 29 Nov 2017, 2:00 am. Otherwise the request will expire automatically and you won't get paid. If the booked dates don't work for you, you can also choose to decline the request. Accept or Decline the booking ( https://www.sharetribe.com/sale/d8893061-c860-4e44-8d4d-61587a497e6e/ ) Can't click the button? Here's the link for your convenience: https://www.sharetribe.com/sale/d8893061-c860-4e44-8d4d-61587a497e6e/ ( https://www.sharetribe.com/sale/d8893061-c860-4e44-8d4d-61587a497e6e/ ) You have received this email notification because you are a member of Bikesoil. If you no longer wish to receive these emails, please contact the Bikesoil team. --- See http://localhost:3535 for the HTML preview. Refresh the browser to reload the template and render a new preview. Type +C to quit. ``` You can now use your normal browser developer tooling to test changes to the template and verify how the responsiveness of the content works. When you make changes to the template, you can just refresh the browser and see the updated preview. ## Sending a preview email If you want to verify the email in an email client software, you can also send the test preview email: ```bash flex-cli notifications send --template process/templates/booking-new-request -m my-marketplace-dev ``` The email is sent to the email address of the admin user that was used in logging in to the CLI: ![Email preview in inbox](./preview-inbox.png) ## Sample email context As you see from the previews above, the templates are rendered with the fixed sample context. The `notifications preview` and `notifications send` commands also support an optional `--context` option that can be used to pass in a custom context JSON. You can see the default context JSON here: [sample-template-context.json](/resources/sample-template-context.json) To change the data that is used for previews, download the sample JSON file and make edits to it: ```diff filename="sample-template-context.json" "customer" : { "id" : "ef7f40d5-da66-489a-957b-641313b68204", "first-name" : "Jane", "last-name" : "Pritchett", - "display-name" : "Jane P", + "display-name" : "Mary P", ``` You can then pass the changed context file to the preview command: ```bash flex-cli notifications preview --template process/templates/booking-new-request --context sample-template-context.json -m my-marketplace-dev ``` Now you will see the preview with the context data that you edited: {/* ![Email preview with custom context](./preview-context.png) */} ```bash {4, 8} Preview fetched Template: booking-new-request Subject: Mary P requested to book Wooden sauna Plain text content: You have a new booking request for Wooden sauna Mary P requested to book Wooden sauna in Bikesoil. ``` You can use the [sample-template-context.json](/resources/sample-template-context.json) file as a base and test how your templates behave with different content or custom extended data etc. ## Push new version Now that you have edited the email templates, you need to push a new version of your process: ```bash flex-cli process push --path process --process default-booking -m my-marketplace-dev ``` You can see the new version in Console or using the `process list` command: ```bash flex-cli process list --process default-booking -m my-marketplace-dev ``` ## Update alias As you saw from Console or from the `process list` command above, there isn't an alias pointing to the latest process version. To allow Sharetribe Web Template or other apps to use the new process version through the Marketplace API, you will need an alias to point to the version. In our `default-booking` example process there is a `release-1` alias. Let's update that to point to the new process version: ```bash flex-cli process update-alias --process default-booking --alias release-1 --version 2 -m my-marketplace-dev ``` To see the updated alias, run the `process list` command again: ```bash flex-cli process list --process default-booking -m my-marketplace-dev ``` ## Summary This is the generic workflow to update the notification email contents that are part of the transaction process of your marketplace. With this and the [Edit transaction process with Sharetribe CLI](/how-to/transaction-process/edit-transaction-process-with-sharetribe-cli/) guide, you now know how to change the transaction process and its email templates. As a next step, you might want to read the [Transaction process format](/references/transaction-process-format/) reference article. --- ## Set up outgoing email settings Path: how-to/emails-and-notifications/set-up-outgoing-email-settings/index.mdx # Set up outgoing email settings Setting up reliable email delivery is crucial for your marketplace to work properly. It is also important to brand your email sending name and address to look professional and polished. We offer two ways of managing email sending from your marketplace. The default option is to rely on a managed service offered by default in Sharetribe. If you wish to obtain more flexibility and control over your email sending, you can also integrate Sharetribe with your own SendGrid account. ## Managed setup Sharetribe uses [SendGrid](https://sendgrid.com/) as an email service provider. By default, we manage your SendGrid account and settings automatically, and the costs are included in your Sharetribe subscription. You can find instructions for configuring your outgoing email address [in the Sharetribe help center](https://www.sharetribe.com/help/en/articles/8704439-how-to-configure-your-outgoing-email-address).
### Configure a DMARC policy Starting in February 2024, [Google](https://blog.google/products/gmail/gmail-security-authentication-spam-protection/) and [Yahoo](https://blog.postmaster.yahooinc.com/post/730172167494483968/more-secure-less-spam) are rolling out changes to bulk email sender requirements. Their aim is to guarantee a more secure experience and a less spammy inbox for email recipients. When following the DNS configuration instructions in our help center, your domain is already configured properly for SPF and DKIM authentication. However, setting up a DMARC policy is recommended, especially if there is significant amount of email sent daily for your domain. Without one, email deliverability to GMail and Yahoo addresses from your domain may be worse. Setting up a DMARC policy involves adding a DNS record for the name `_dmarc` in your domain. For example, if your domain is `example.com`, then the DNS record should be for the name `_dmarc.example.com`. The type of the record must be `TXT`. The value of the record can be different depending on your or your organization's requirements. If unsure, use the following value as your DMARC policy: `v=DMARC1; p=none`. This is a minimal neutral DMARC policy. For example, for `example.com` the record would be: | type | name | value | | ---- | ------------------- | ---------------- | | TXT | \_dmarc.example.com | v=DMARC1; p=none | Note that depending on your DNS hosting provider, the domain name may be added automatically to any records you create. In that case you need to use just `_dmarc` as the record name, without the domain name. You may also need to place the value in quotes: `"v=DMARC1; p=none"`. #### Using a strict DMARC policy If you prefer to set a more strict DMARC policy, you can do so. Note, however, that Sharetribe's email integration requires that the DMARC policy specifies "relaxed alignment" for SPF and DKIM checks. This means you should NOT configure a policy with the values `aspf=s` (strict SPF alignment) or `adkim=s` (strict DKIM alignment). ## Using your own SendGrid account If you have already set up your outgoing email settings using the managed setup and want to switch to using your own SendGrid account, please contact our [support](mailto:hello@sharetribe.com). We will reset your outgoing email settings, and you will be able to reconfigure using SendGrid. In addition to using the managed email sending, you have the option to choose to use your own SendGrid account by integrating it with Sharetribe. You might want to do this because: - You want to see exactly what emails are sent from your marketplace and fix problems (like bounces) with users' email addresses. - You want to see open rates and other statistics about the email notifications that were sent. - You are also using SendGrid for your newsletters and other marketing emails, and want to have all your email-related data in one place. - You want to use your own IP address for email sending and reputation. - You want to verify user email addresses programmatically, using the [`users/verify_email`](https://www.sharetribe.com/api-reference/integration.html#verify-email-address) Integration API endpoint (you can use this endpoint in `dev` and `test` without your own SendGrid account). If you'd prefer to get access to such features and statistics, this is possible by connecting your own SendGrid account to Sharetribe. Just remember that doing this means you'd be subject to SendGrid's standard pricing for all outgoing emails. You can only configure your own SendGrid account in a `live` environment. Follow the steps below to set up your own SendGrid account for email sending: ### Create an API key in SendGrid Please, refer to the SendGrid instructions on [key management](https://sendgrid.com/docs/ui/account-and-settings/api-keys/#managing-api-keys). You should create a key with `Restricted Access` with `Mail Send` configured with `Full Access` permission. Make sure that your key has the correct permission in place, otherwise email sending from Sharetribe will not work. Make note of the key while creating it, it will be shown only once. Also, you need to make sure that you have configured your domain in SendGrid by following their instructions on [how to configure domain authentication](https://sendgrid.com/docs/ui/account-and-settings/how-to-set-up-domain-authentication/). ### Enable using own SendGrid key in Sharetribe Console Log in to your Console, and select your Live environment. There are two options for integrating SendGrid. You can do it during the initial setup of the marketplace or later if you wish to change from managed setup to using your own SendGrid account. **During initial setup** Go to the [General > Outgoing email address](https://console.sharetribe.com/general/outgoing-email-address) page in the Build section. You will see a choice between managed email sending or Advanced Setup: ![Initial setup](./initial-setup.png) Choose "Update email sender settings" and you will be able to add your SendGrid API key. Submit the form and the setup is complete. ## Changing from one email sending mode to another If you have previously completed the outgoing email settings for one type of email sending, and you want to switch to another type, please contact our [support](mailto:hello@sharetribe.com). We will reset your outgoing email settings, and you will be able to reconfigure them yourself. --- ## How to implement a like feature using events Path: how-to/events/like-feature/index.mdx # How to implement a like feature using events Events represent changes in marketplace data resources. In this guide, we use events to listen to changes in user extended data. We react to these events by updating a 'likes' value in listing extended data. In addition, we display the number of likes on the listing page and allow users to interact by clicking on a 'like' -button. ## Approaches to updating extended data [Extended data](/references/extended-data/) is a practical feature that can be used to store structured data associated with either listings, users or transactions. When implementing a like-counter, it's logical to store the number of likes associated with a listing in the listing's extended data. We can easily access the number of likes by querying the [query-listings](https://www.sharetribe.com/api-reference/marketplace.html#query-listings) endpoint and passing the relevant listing ID as a query parameter. However, merely storing the number of likes in the listing's extended data doesn't provide information about which user has liked the listing. If we can associate likes with users, we can build a "liked listings" feature and show the user a list of listings they have liked. A naive approach would be to store the user ID in the listing's extended data each time they like a listing. However, to render a list of liked listings, we would need to loop through all listings to find all occurrences of the user ID. That is why in this guide, in addition to storing the number of likes in the listing's extended data, we choose to save the listing ID in the user's extended data. To allow users to like listings, we introduce a UI-element users can interact with to like a listing. However, when we update a listing's extended data as a reaction to user input, we are prone to a race condition. Fortunately, Sharetribe provides a powerful feature called [events](/references/events/) that we can use to solve the problem. We can listen to events using a script that polls the [events endpoint](https://www.sharetribe.com/api-reference/integration.html#query-events). As events are handled sequentially, the script can update the listing's extended data while avoiding race conditions. A downside to using events is that the like count may take a moment to update. We can mask this delay by temporarily incrementing the like value in the UI. ## Create a UI component the user can interact with We start by creating an icon that users can interact with to like or dislike a listing. Next to the icon, we'll display the number of likes. For the UI component, we'll create a new subcomponent _SectionLikes.js_ in the _ListingPage_ directory. ### Create a new file The Sharetribe Web Template features two different listing page layouts. In this tutorial, we will implement the like feature to the default ListingPageCarousel version, but you can just as well follow the instructions to implement it in ListingPageCoverPhoto as well. Start by creating a new file, called `SectionLikes.js`: ### Add the new subcomponent We'll create a new component that renders an svg and the amount of likes: ```jsx const IconHeart = () => { return ( ); }; const SectionLikes = (props) => { const { publicData } = props; const likes = publicData?.likes ? publicData.likes : 0; return ( {likes} ); }; export default SectionLikes; ``` ### Import the component Next, let's import the new component in `ListingPageCarousel.js`: ```jsx filename="ListingPageCarousel.js" ``` ### Assign an instance of the component into a variable We want to show the like button next to the listing title. In the template, where that title is shown depends on whether the listing page is viewed on mobile or on desktop. Because of this, we want to set the whole instance into a constant, so we can pass it to the correct contexts. ```jsx filename="ListingPageCarousel.js" const sectionLikes = ( ); ``` ### Pass the component variable to the render method and OrderPanel We've created the new component _SectionLikes.js_, but it still needs to be included in the render method of _ListingPageCarousel.js_ to show it in a mobile layout: ```jsx filename="ListingPageCarousel.js" {5}

{sectionLikes}
``` To show the like component on a desktop layout, we need to pass it as a prop to OrderPanel. ```jsx filename="ListingPageCarousel.js" {5}
``` ### Render the like section in OrderPanel.js To show the like component in OrderPanel, we need to pick it from props and show it next to the title. Since we are passing the full instance as a prop, you can show it as a part of the heading. ```jsx filename="OrderPanel.js" {6, 12} const OrderPanel = props => { const { rootClassName, className, /* ... */ sectionLikes, } = props; /* ... */
{titleDesktop ? titleDesktop :

{title}

} {subTitleText ?
{subTitleText}
: null} {sectionLikes}
``` ### Update ListingPage.module.css Finally, let's style our new component by adding the following CSS rules in `ListingPage.module.css`: ```css filename="ListingPage.module.css" .heartIcon { border-radius: 11px; cursor: pointer; display: inline-block; padding: 5px; &:hover { background-color: #e3e1e1; } } .heartDisabled > svg { fill: var(--marketplaceColorLight); } .heartIcon > svg { fill: #fdb7b0; transition: all 0.2s; } .iconLiked > svg { fill: var(--marketplaceColorLight); transition: all 0.2s; } ``` Now, you should be able to see a heart-shaped icon with a like counter next to it if you navigate to the listing page: ![Example of heart icon](./heart-icon.png 'Example of heart icon') ## Update user extended data To update the user extended data, we'll need to make some changes to `ListingPage.duck.js`. We'll need to import the `currentUserShowSuccess` function from `user.duck.js` to update the current user. In addition, we'll be adding a new action type, action creator and reducer to the file. For more information on how Redux is setup in the template, refer to our [article on Redux](/template/state-management/redux/). ### Import setCurrentUser ```jsx filename="ListingPage.duck.js" {4} fetchCurrentUser, fetchCurrentUserHasOrdersSuccess, setCurrentUser, } from '../../ducks/user.duck'; ``` ### Add new variables to initialState: ```jsx filename="ListingPage.duck.js" {6-7} const initialState = { id: null, showListingError: null, reviews: [], fetchReviewsError: null, updateLikesError: null, updateLikesInProgress: false, monthlyTimeSlots: {}, timeSlotsForDate: {}, lineItems: null, fetchLineItemsInProgress: false, fetchLineItemsError: null, sendInquiryInProgress: false, sendInquiryError: null, inquiryModalOpenForListingId: null, }; ``` ### Add new thunk Create a thunk that adds the listing ID to the user extended data. ```jsx filename="ListingPage.duck.js" const updateLikesPayloadCreator = ({ listingId }, thunkAPI) => { const { dispatch, rejectWithValue, getState, extra: sdk } = thunkAPI; return dispatch(fetchCurrentUser()).then(() => { const currentUser = getState().user.currentUser; const currentLikes = currentUser?.attributes?.profile?.privateData?.likedListings; const queryParams = { expand: true, include: ['profileImage'], 'fields.image': [ 'variants.square-small', 'variants.square-small2x', ], }; // if listingId already exists in currentLikes, it should be removed from currentLikes // if user has current likes, merge listingId into current likes const ifDislike = !!currentLikes?.includes(listingId); const likedListings = ifDislike ? currentLikes.filter((id) => id !== listingId) : currentLikes ? [...currentLikes, listingId] : [listingId]; return sdk.currentUser .updateProfile({ privateData: { likedListings } }, queryParams) .then((response) => { const entities = denormalisedResponseEntities(response); if (entities.length !== 1) { throw new Error( 'Expected a resource in the sdk.currentUser.updateProfile response' ); } const currentUser = entities[0]; // Update current user in state.user.currentUser through user.duck.js dispatch(setCurrentUser(currentUser)); }) .catch((e) => { return rejectWithValue(storableError(e)); }); }); }; export const updateLikesThunk = createAsyncThunk( 'ListingPage/updateLikes', updateLikesPayloadCreator ); export const updateLikes = (listingId) => (dispatch, getState, sdk) => { return dispatch(updateLikesThunk({ listingId })).unwrap(); }; ``` ### Update the reducer: ```jsx filename="ListingPage.duck.js" .addCase(updateLikesThunk.pending, (state, action) => { state.updateLikesInProgress = true; }) .addCase(updateLikesThunk.fulfilled, (state, action) => { state.updateLikesInProgress = false; }) .addCase(updateLikesThunk.rejected, (state, action) => { state.updateLikesError = action.payload; state.updateLikesInProgress = false; }) ``` ### Import updateLikes on the Listing Page We need to import the new thunk we defined in the _ListingPage.duck.js_ file into _ListingPageCarousel.js_ in order to connect to the Redux store through _mapDispatchToProps_: ```jsx filename="ListingPageCarousel.js" {6} sendInquiry, setInitialValues, fetchTimeSlots, fetchTransactionLineItems, updateLikes, } from './ListingPage.duck'; ``` ### Expose state values on the Listing Page We need to expose the props that are connected to the Redux store in `ListingPageCarousel.js`: ```jsx filename="ListingPageCarousel.js" {18-19} return { isAuthenticated, currentUser, getListing, getOwnListing, scrollingDisabled: isScrollingDisabled(state), inquiryModalOpenForListingId, showListingError, reviews, fetchReviewsError, monthlyTimeSlots, // for OrderPanel timeSlotsForDate, // for OrderPanel lineItems, // for OrderPanel fetchLineItemsInProgress, // for OrderPanel fetchLineItemsError, // for OrderPanel sendInquiryInProgress, sendInquiryError, updateLikesInProgress, updateLikesError, }; }; ``` ```jsx filename="ListingPageCarousel.js" {15-16} const mapStateToProps = state => { const { isAuthenticated } = state.auth; const { showListingError, reviews, fetchReviewsError, monthlyTimeSlots, timeSlotsForDate, sendInquiryInProgress, sendInquiryError, lineItems, fetchLineItemsInProgress, fetchLineItemsError, inquiryModalOpenForListingId, updateLikesInProgress, updateLikesError, } = state.ListingPage; ``` ### Connect updateLikes to mapDispatchToProps function ```jsx filename="ListingPageCarousel.js" {7} const mapDispatchToProps = (dispatch) => ({ onManageDisableScrolling: (componentId, disableScrolling) => dispatch(manageDisableScrolling(componentId, disableScrolling)), callSetInitialValues: ( setInitialValues, values, saveToSessionStorage ) => dispatch(setInitialValues(values, saveToSessionStorage)), onFetchTransactionLineItems: (params) => dispatch(fetchTransactionLineItems(params)), onUpdateLikes: (listingId) => dispatch(updateLikes(listingId)), onSendInquiry: (listing, message) => dispatch(sendInquiry(listing, message)), onInitializeCardPaymentData: () => dispatch(initializeCardPaymentData()), onFetchTimeSlots: (listingId, start, end, timeZone, options) => dispatch(fetchTimeSlots(listingId, start, end, timeZone, options)), }); ``` ## Update likes when clicking on the icon Currently, clicking on the icon will do nothing. We need to make some small tweaks for the button click to successfully update the user extended data. First, we'll need to define the necessary props on _SectionLikes_. Then we'll add an onClick event handler to the _SectionLikes_ React component. ### Pass SectionLikes the new props ```jsx filename="ListingPageCarousel.js" {3-7} const sectionLikes = ( ); ``` ### Define props in SectionLikes.js ```jsx filename="SectionLikes.js" {4-7} const SectionLikes = props => { const { publicData, onUpdateLikes, listingId, currentUser, updateLikesInProgress, } = props; ``` ### Define currentLikes in SectionLikes.js ```jsx filename="SectionLikes.js" const currentLikes = currentUser?.attributes?.profile?.privateData?.likedListings; ``` ### Add an onClick event handler ```jsx filename="SectionLikes.js" {2-5} { if (!updateLikesInProgress && currentUser) { onUpdateLikes(listingId); } }}> ``` Now, clicking on the icon will either add or remove the listing to the user extended data with the key _likedListings_. However, as liked listings are only saved into user extended data at the moment, the number of likes still remains at zero. ## Listening to events Using events we can react to changes in users' extended data. To do this we'll listen to the [/events/query](https://www.sharetribe.com/api-reference/integration.html#query-events) endpoint, filter out relevant events and finally update listing extended data. We'll use the [_notify-new-listings.js_](https://github.com/sharetribe/integration-api-examples/blob/master/scripts/notify-new-listings.js) script in the integration-api-examples as a basis for our new script. Make sure to follow the instructions at the [root of the repository](https://github.com/sharetribe/integration-api-examples#getting-started) if you're unsure how to run the script locally. First off, we'll need to change what event type we want to filter. In our case, it's _user/updated_. ### Filter events by user/updated ```js {2} showLineNumbers{55} const queryEvents = (args) => { var filter = { eventTypes: 'user/updated' }; return integrationSdk.events.query({ ...args, ...filter }); }; ``` ### Add a function that updates the 'likes' value: Next, we'll add a function that updates the 'likes' value in listing extended data by calling the [Integration API listings/update](https://www.sharetribe.com/api-reference/integration.html#update-listing) endpoint: ```js /** * @param {string} listingId * @param {number} likeAddition – A value representing a like or a dislike that is either added to or subtracted from currentLikes */ const updateListing = (listingId, likeAddition) => { return integrationSdk.listings .query({ ids: listingId, }) .then((listings) => { const listing = listings.data.data[0]; const currentLikes = listing.attributes.publicData.likes || 0; const updatedLikes = currentLikes + likeAddition; return integrationSdk.listings.update( { id: listingId, publicData: { likes: updatedLikes, }, }, { expand: true } ); }); }; ``` ### Add helper functions: We'll no longer need the _analyzeEvent_ or _isPublished_ functions found in the boilerplate code. Instead, let's add a few new functions. These functions will help us determine if the event we've received is a like or dislike, and reduce multiple likes to a single API call: ```js // Get the difference between two arrays const getDifference = (arr1, arr2) => { return arr1.filter((x) => !arr2.includes(x)); }; // Compare the amount of likes in the previous event to the current one to // determine which listing was liked or disliked const getLikedListingId = (previousLikes, currentLikes) => { if (previousLikes === null) return currentLikes; if (currentLikes === null) return previousLikes; else return previousLikes.length < currentLikes.length ? getDifference(currentLikes, previousLikes) : getDifference(previousLikes, currentLikes); }; const getLikeCount = (previousLikes, currentLikes) => { return previousLikes === null || previousLikes.length < currentLikes.length ? 1 : -1; }; // Reducer returns an object with listing ID's as keys and amount of likes as values const groupEvents = (events) => { return (likesToBeUpdated = events.reduce((likes, event) => { const { resource: user, previousValues } = event.attributes; // we might have a user/updated event that doesn't target likedListings if ( !previousValues.attributes?.profile?.privateData?.likedListings ) { return {}; } const { likedListings: previouslyLikedListings } = previousValues.attributes.profile.privateData || {}; const likedListings = user.attributes.profile?.privateData?.likedListings; const likeCount = getLikeCount( previouslyLikedListings, likedListings ); const listingId = getLikedListingId( previouslyLikedListings, likedListings ); likes[listingId] = likes[listingId] ? likes[listingId] + likeCount : likeCount; return likes; }, {})); }; ``` Finally, to call the right functions, let's make a few changes to the `pollLoop` function: ```diff const pollLoop = (sequenceId) => { var params = sequenceId ? {startAfterSequenceId: sequenceId} : {createdAtStart: startTime}; queryEvents(params) .then(res => { const events = res.data.data; const lastEvent = events[events.length - 1]; const fullPage = events.length === res.data.meta.perPage; const delay = fullPage? pollWait : pollIdleWait; const lastSequenceId = lastEvent ? lastEvent.attributes.sequenceId : sequenceId; - events.forEach(e => { - analyzeEvent(e); - }); - - if (lastEvent) saveLastEventSequenceId(lastEvent.attributes.sequenceId); - setTimeout(() => {pollLoop(lastSequenceId);}, delay); + const likesToUpdate = groupEvents(events); + const actions = Object.keys(likesToUpdate).map(key => updateListing(key, likesToUpdate[key])); + + const results = Promise.all(actions); + results.then(result => { + result.forEach(el => { + console.log(`Listing ID ${el.data.data.id.uuid} now has ${el.data.data.attributes.publicData.likes} like(s).`) + }) + + if (lastEvent) saveLastEventSequenceId(lastEvent.attributes.sequenceId); + setTimeout(() => {pollLoop(lastSequenceId);}, delay); + }) + }); }; ``` You can now run the script locally following the instructions [here](https://github.com/sharetribe/integration-api-examples#getting-started). ## Increment the counter in the UI You can now click on the likes button, and the number of likes is updated by the script polling the events endpoint. However, the like count doesn't get updated immediately due to the slight latency in our events listener. Using React state, we can temporarily increment the counter. Adding a temporary increment isn't mandatory as the number of likes shown to the user will eventually be consistent with the value in extended data. However, we can provide the user with instant feedback by updating the like count in the front-end, even though our event listener updates the actual like count. ### Add new state to ListingPageCarousel.js ```jsx filename="ListingPageCarousel.js" {6} export const ListingPageComponent = props => { const [inquiryModalOpen, setEnquiryModalOpen] = useState( props.inquiryModalOpenForListingId === props.params.id ); const [likesOffset, updateLikesOffset] = useState(0); ``` ### Pass new functions as props to SectionLikes ```jsx filename="ListingPageCarousel.js" {8-10} const sectionLikes = ( updateLikesOffset(likesOffset - 1)} onAddLike={() => updateLikesOffset(likesOffset + 1)} /> ); ``` ### Define new props in SectionLikes.js ```jsx filename="SectionLikes.js" {8-10} const SectionLikes = props => { const { publicData, onUpdateLikes, listingId, currentUser, updateLikesInProgress, likesOffset, onAddLike, onSubtractLike, } = props; ``` ### Include logic to increment likes and conditional styling ```jsx filename="SectionLikes.js" {2, 4, 7, 11-16, 20} const currentLikes = currentUser?.attributes?.profile?.privateData?.likedListings; const alreadyLiked = currentLikes?.includes(listingId); const likes = publicData?.likes ? publicData.likes : 0; const classes = classNames(currentUser ? css.heartIcon : css.heartDisabled, alreadyLiked ? css.iconLiked : null) return ( { if (!updateLikesInProgress && currentUser) { onUpdateLikes(listingId); if (alreadyLiked) { onSubtractLike(); } else { onAddLike(); } } }}> { likes + likesOffset } ``` Now while running the event listener script you should have a fully functional like button. If you're interested in reading more about events, you can read [our articles on reacting to events](/how-to/events/reacting-to-events/) and [user Events as triggers in Zapier](https://www.sharetribe.com/help/en/articles/8529989#h_1197b66f66). --- ## Reacting to events Path: how-to/events/reacting-to-events/index.mdx # Reacting to events One of the main purposes of [events](/references/events/) in Sharetribe is to allow building integrations that programmatically react to changes and actions in a marketplace. In this guide, we will show how to use the Sharetribe Integration API to continuously query events efficiently. We will also demonstrate how to interpret the event data to detect actions, using publishing a listing as an example. This guide assumes that you have already set up a Sharetribe Integration API application and have a Node.js app with the [Integration API SDK](/concepts/api-sdk/js-sdk/) ready. If you have not yet used the Sharetribe Integration API, follow the [Getting started with the Integration API](/introduction/getting-started-with-integration-api/) guide first and you will be ready to proceed with this guide. In this guide we will cover the following main topics: - querying events via the Integration API, using filters to receive only relevant events - using event data to determine the change that happened - understanding event sequence IDs and using them to correctly query for new events - understanding at-least-once and at-most-once types of event processing ## Querying events The Integration API [/events/query](https://www.sharetribe.com/api-reference/integration.html#query-events) endpoint will be used at the core of this guide. In the most basic form, a query for all available events using the SDK looks like this: ```jsx integrationSdk.events.query(); ``` In this guide, we are interested in detecting when a new listing is published and therefore do not need to process all events that happen in the marketplace. Instead, we can filter out only the relevant listing-related events. Recall that the [listing `state`](https://www.sharetribe.com/api-reference/integration.html#listing-states) attribute indicates whether a listing is `draft` or `published` (among other possible states) and that a new listing may become published in one of three ways: - a listing can be [created as published](https://www.sharetribe.com/api-reference/marketplace.html#create-listing) - a listing can be created as [draft](https://www.sharetribe.com/api-reference/marketplace.html#create-draft-listing) and be [published](https://www.sharetribe.com/api-reference/marketplace.html#publish-draft-listing) separately - in case your marketplace has the [listing approval feature](https://www.sharetribe.com/help/en/articles/8413545-approve-listings-before-publishing) on, a listing that is pending approval becomes published when it is approved by an operator Therefore, in order to cover all cases, we need to process both `listing/created` and `listing/updated` events. A query for only these types of events looks like this: ```jsx integrationSdk.events.query({ eventTypes: 'listing/created,listing/updated', }); ``` This would still give us all events with the given types that Sharetribe keeps in history. In practice, often we are interested in _new_ events only. In the absence of another point of reference, it is possible to query events starting from a given timestamp: ```jsx const now = new Date(); const fiveMinutesAgo = new Date(now - 300000); integrationSdk.events.query({ createdAtStart: fiveMinutesAgo, eventTypes: 'listing/created,listing/updated', }); ``` Later in the guide, we will see how to use the data of events that the application has already processed to query strictly for subsequent events. ## Using event data to detect change Even with the event type filtering in place, not all `listing/created` or `listing/updated` events represent the logical change we may be interested in (such as a listing being published for the first time). We need to use the event [data](/references/events/#event-data) and in particular the [resource and previousValues](/references/events/#resource-data-and-previous-values) to detect which events correspond to the change we want to react to. A new listing is first published when one of these happen: - a `listing/created` event shows that the current listing state is `published` - a `listing/updated` event shows that the current listing state is `published` and the previous state was `draft` - a `listing/updated` event shows that the current listing state is `published` and the previous state was `pendingApproval` In code, analyzing the event data can be done like this: ```jsx const now = new Date(); const fiveMinutesAgo = new Date(now - 300000); const handleListingPublished = (event) => { const { resourceId, resource: listing } = event.attributes; const listingId = resourceId.uuid; const authorId = listing.relationships.author.data.id.uuid; // Do something about the new published listing, such as send notification, // synchronize data to external system, etc. console.log( `A new listing has been published: listingId ${listingId}, author ID: ${authorId}` ); }; const analyzeEvent = (event) => { const { resource: listing, previousValues, eventType, } = event.attributes; const listingState = listing.attributes.state; const { state: previousState } = previousValues.attributes || {}; const isPublished = listingState === 'published'; const isPendingApproval = listingState === 'pendingApproval'; const wasDraft = previousState === 'draft'; const wasPendingApproval = previousState === 'pendingApproval'; switch (eventType) { case 'listing/created': if (isPublished) { handleListingPublished(event); } break; case 'listing/updated': if (isPublished && (wasPendingApproval || wasDraft)) { handleListingPublished(event); } break; } }; integrationSdk.events .query({ createdAtStart: fiveMinutesAgo, eventTypes: 'listing/created,listing/updated', }) .then((res) => { const events = res.data.data; events.forEach(analyzeEvent); }); ``` Naturally, instead of simply logging the event, the application can be doing something else, such as sending an email notification to a marketplace operator, synchronising data with an external system and so on. ## Polling events continuously using sequence IDs In the guide so far we saw how to process events from a single query. In practice, an application that reacts to events needs to poll for new events continuously. In this section we will show how this is best achieved. In Sharetribe, each event has a unique `sequenceId` and the [sequence IDs are strictly increasing](/references/events/#event-sequence-ids). Moreover, the `/events/query` Integration API endpoint always returns events sorted by their sequence IDs in ascending order. This means that once a batch of events is processed, the sequence ID of the last event can be used to query for strictly newer events, using the `startAfterSequenceId` [query parameter](https://www.sharetribe.com/api-reference/integration.html#query-events). For example: ```jsx // Given the sequence ID of last processed event, query only for newer events const lastEventSequenceId = 1234; integrationSdk.events.query({ startAfterSequenceId: lastEventSequenceId, eventTypes: 'listing/created,listing/updated', }); ``` Using that knowledge, a continuous polling loop works like this: 1. start querying events from some point in time, such as the current timestamp 2. if there are some new events, process them and note the sequence ID of the last one 3. after processing the batch, wait for a time and poll again using the sequence ID of the last event (if there was one) or the same timestamp as in the original query and go to step 2. ### Persisting the last seen sequence ID The polling loop described above works for a single execution of the application. However, if the application is stopped and then restarted, some events may be missed. To remedy that, the application needs to persist somehow the sequence ID of the last processed event. How that is achieved, depends on the particular application and available infrastructure, but some options include: - store the sequence ID in a local file - store the sequence ID in some cloud storage, such as AWS S3, Azure Blob Storage or GCP Cloud Storage - store the sequence ID in some database With the sequence ID persisted, subsequent executions of the application can resume polling events from the exactly correct point of reference and will therefore not miss any events that may have happened in the meantime. ### Recommended polling interval A suitable polling interval that applications should use depend on a few factors, including: - how much activity there is in the marketplace - how important it is that the events are processed without delay - maintaining good practice when accessing the Sharetribe APIs In most cases, a polling interval of 1-10 minutes may be completely sufficient. In cases were more rapid reaction is required, we highly recommend that the polling interval should not be smaller than 10-30 seconds. Note that a single query returns up to a 100 events. If a query returns a full page of events, there may be more events already available for querying. In that case, a smaller timeout is acceptable, so that the application can process all available events in a timely manner (e.g. 250-1000 ms). ### Poll loop example using local file to store state Below is a full polling loop example, using a local file to store the last processed event's sequence ID: ```jsx const fs = require('fs'); // Start polling from current time on, when there's no stored state const startTime = new Date(); // Polling interval (in ms) when all events have been fetched. const pollIdleWait = 300000; // 5 minutes // Polling interval (in ms) when a full page of events is received and there may be more const pollWait = 1000; // 1s // File to keep state across restarts. Stores the last seen event sequence ID, // which allows continuing polling from the correct place const stateFile = './last-sequence-id.state'; const queryEvents = (args) => { var filter = { eventTypes: 'listing/created,listing/updated' }; return integrationSdk.events.query({ ...args, ...filter }); }; const saveLastEventSequenceId = (sequenceId) => { // Save state to local file try { fs.writeFileSync(stateFile, sequenceId); } catch (err) { throw err; } }; const loadLastEventSequenceId = () => { // Load state from local file, if any try { const data = fs.readFileSync(stateFile); return parseInt(data, 10); } catch (err) { return null; } }; const handleEvent = (event) => { // detect change and handle event // ... // Then store the event's sequence ID saveLastEventSequenceId(event.attributes.sequenceId); }; const pollLoop = (sequenceId) => { var params = sequenceId ? { startAfterSequenceId: sequenceId } : { createdAtStart: startTime }; queryEvents(params).then((res) => { const events = res.data.data; const fullPage = events.length === res.data.meta.perPage; const delay = fullPage ? pollWait : pollIdleWait; const lastEvent = events[events.length - 1]; const lastSequenceId = lastEvent ? lastEvent.attributes.sequenceId : sequenceId; events.forEach((e) => { handleEvent(e); }); setTimeout(() => { pollLoop(lastSequenceId); }, delay); }); }; // Load state from local file, if any const lastSequenceId = loadLastEventSequenceId(); // kick off the polling loop pollLoop(lastSequenceId); ``` ## At-least-once and at-most-once event processing No system ever operates without failures and therefore applications should account for possible failure conditions during event processing. For instance, an API call to another system during event handling may fail (sending notification, synchronizing state, executing other actions, etc), or the application itself may crash due to a bug or an issue on the host system. An important aspect of this is what is often referred to as _at-least-once_ and _at-most-once_ processing in message processing systems. Consider the following scenarios: 1. the application receives an event, records persistently its sequence ID and then proceeds to handle the event 2. the application receives an event, handles it fully and then records its sequence ID persistently Suppose that a failure occurs during the event handling and the application crashes. In scenario 1, when the application is restarted, it will query for new events, skipping over the event that failed to be handled, as its sequence ID is already stored. Depending on when exactly the failure occurred, the event may not have been fully handled and therefore will be left unprocessed (this is at-most-once processing). On the other hand, in scenario 2, when the application is restarted, it will retry handling the failed event (since the last recorded sequence ID is that of some previous event) and potentially may process the event a second time (that is, at-least-once processing). The preferable strategy for an application depends on the exact event handling logic. For example, at-least-once processing may be preferable in any of the following situations: - when the event processing is idempotent, meaning that processing and event more than once does not produce undesired side-effects - when it is tolerable that some events may be processed multiple times (e.g. when a user may receive an occasional duplicate notification) On the other hand, at-most-once processing is preferable if processing an event produces a side-effect that must not be repeated (such as making a money transfer) and can not be performed safely in an idempotent manner. In those cases, it may be desirable to handle the failed events somehow manually and resume automatic event processing only for subsequent events. Note that, if events are processed in batches and only the sequence ID of the last event in the batch is recorded, the effect of potential failure may be amplified to affect all events in the failed batch. Again, whether that is acceptable depends on the concrete event handling mechanics. Finally, _exactly-once_ processing may not be achievable in most scenarios when idempotent operations are impossible. That is especially true when event handling requires working across multiple systems (such as API calls to 3rd party services). In practice, at-most-once processing can be considered the safer choice in those cases. ## Additional resources - A [full example](https://github.com/sharetribe/integration-api-examples/blob/master/scripts/notify-new-listings.js) Integration API application is available [in the Integration API examples](https://github.com/sharetribe/integration-api-examples/) repository - [Reference article](/references/events/) for events --- ## View events with Sharetribe CLI Path: how-to/events/view-events-with-sharetribe-cli/index.mdx # View events with Sharetribe CLI The Sharetribe CLI (Command-line interface) is a tool for examining and changing your marketplace's advanced configurations such as transaction processes and email templates. This guide expects that you have already installed Sharetribe CLI and are logged in with your API key. If not, it's recommended to first read the tutorial [Getting started with Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/). In this guide, we will learn how the events of a marketplace can be queried using Sharetribe CLI, how we can inspect events in more detail as well as combine Sharetribe CLI with other tools to further process the event data. In Sharetribe, events represent changes in marketplace data resources such as listings, users and transactions. An event captures a single change in marketplace data, e.g. a user being created or a listing being updated. Events can be further analyzed to interpret them as logical actions such as a listing being published, a message being sent or a user having changed their email address by looking into what were the changed data fields. To learn more about events, have a look at the [Events](/references/events/) reference. ## Querying events The command for querying events using Sharetribe CLI is `events`. ```bash $ flex-cli help events Get a list of events. USAGE $ flex-cli events OPTIONS --after-seqid=SEQUENCE_ID Show events with sequence ID larger than (after) the specified. --after-ts=TIMESTAMP Show events created after the given timestamp, e.g. '--after-ts 2020-10-10' or '--after-ts 2020-10-10T10:00.000Z' --before-seqid=SEQUENCE_ID Show events with sequence ID smaller than (before) the specified. --before-ts=TIMESTAMP Show events created before the given timestamp, e.g. '--before-ts 2020-11-15' or '--before-ts 2020-11-15T12:00.000Z' --filter=EVENT_TYPES Show only events of given types, e.g. '--filter listing/updated,user'. --json Print full event data as one JSON string. --json-pretty Print full event data as indented multi-line JSON string. --related-resource=RELATED_RESOURCE_ID Show events that are related to a specific resource ID. --resource=RESOURCE_ID Show events for a specific resource ID only. --seqid=SEQUENCE_ID Get only the event with the given sequence id. -l, --limit=NUMBER Show given number of events (default and max is 100). Can be combined with other parameters. -m, --marketplace=MARKETPLACE_ID marketplace identifier ``` Events command supports various ways to query events. Querying without any of the additional parameters lists the 100 latest events for your marketplace: ```bash $ flex-cli events -m my-marketplace-dev Seq ID Resource ID Event type Created at local time Source Actor 3391589 5fca1e5b-2004-4479-a68c-dfc8a03083b8 availabilityException/created 2020-12-04 1:32:43 PM marketplace-api jane@example.com 3391590 5fca1e5c-eda8-4f54-ac30-ee7fe1010d11 availabilityException/created 2020-12-04 1:32:44 PM marketplace-api jane@example.com 3462923 5dfb4a42-8937-47b5-b482-44679828939c user/updated 2020-12-07 3:17:30 PM console joe@example.com 3471843 5fce8536-61f5-4c85-8160-61b1799d256f user/updated 2020-12-07 9:45:38 PM marketplace-api joe@example.com 3471856 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/created 2020-12-07 9:47:19 PM marketplace-api joe@example.com 3471857 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:28 PM marketplace-api joe@example.com 3471858 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:38 PM marketplace-api joe@example.com 3471859 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:53 PM marketplace-api joe@example.com 3471860 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:56 PM marketplace-api joe@example.com 3471863 5fce8728-0f15-46e4-8ee2-c4955e0cc079 availabilityException/created 2020-12-07 9:48:56 PM marketplace-api joe@example.com 3471864 5fce872f-4081-4ca7-a40d-6eb2b003dcc2 availabilityException/created 2020-12-07 9:49:03 PM marketplace-api joe@example.com 3471865 5fce8730-c750-4427-bc58-09f685cc0b03 availabilityException/created 2020-12-07 9:49:04 PM marketplace-api joe@example.com 3471866 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:49:11 PM marketplace-api joe@example.com 3471873 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:50:35 PM marketplace-api joe@example.com 3471875 5fce8536-61f5-4c85-8160-61b1799d256f user/updated 2020-12-07 9:50:52 PM marketplace-api joe@example.com 3471890 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:53:15 PM marketplace-api joe@example.com # ... ``` The default table output mode lists a few key [event attributes](/references/events/#event-attributes). It also shows an email address of the actor, i.e. the person who took the action that caused the event. By adding the `--limit` (short version `-l`) parameter we can limit the output to given number of events: ```bash $ flex-cli events -l 2 -m my-marketplace-dev Seq ID Resource ID Event type Created at local time Source Actor 3391589 5fca1e5b-2004-4479-a68c-dfc8a03083b8 availabilityException/created 2020-12-04 1:32:43 PM marketplace-api jane@example.com 3391590 5fca1e5c-eda8-4f54-ac30-ee7fe1010d11 availabilityException/created 2020-12-04 1:32:44 PM marketplace-api jane@example.com ``` We can look at only certain type of events using the `--filter` parameter: ```bash $ flex-cli events --filter user/created,listing -m my-marketplace-dev Seq ID Resource ID Event type Created at local time Source Actor 3471813 5fce8536-61f5-4c85-8160-61b1799d256f user/created 2020-12-07 9:40:38 PM marketplace-api 3471856 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/created 2020-12-07 9:47:19 PM marketplace-api joe@example.com 3471857 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:28 PM marketplace-api joe@example.com # ... ``` Filtering accepts multiple event types separated with commas. Event types are of the form `RESOURCE_TYPE/EVENT_SUBTYPE`. You can filter with full event type name or by only the resource type. For more information about supported event types, see reference for [supported event types](/references/events/#supported-event-types). Using the `--resource` parameter we can query only events that are for a certain known resource: ```bash $ flex-cli events --resource 5fce86c7-e435-4047-ab3b-dc4fee02d51d -m my-marketplace-dev Seq ID Resource ID Event type Created at local time Source Actor 3471856 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/created 2020-12-07 9:47:19 PM marketplace-api joe@example.com 3471857 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:28 PM marketplace-api joe@example.com 3471858 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:38 PM marketplace-api joe@example.com 3471859 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:53 PM marketplace-api joe@example.com 3471860 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:56 PM marketplace-api joe@example.com 3471866 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:49:11 PM marketplace-api joe@example.com 3471873 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:50:35 PM marketplace-api joe@example.com 3471890 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:53:15 PM marketplace-api joe@example.com ``` In this case the resource ID we passed in was a listing ID. This is useful when we want to investigate the change history of a specific resource. Sometimes it is useful to find events not only for a specific resource but also for other resources related to the specific resource. That is possible using the `--related-resource` parameter: ```bash $ flex-cli events --related-resource 5fce86c7-e435-4047-ab3b-dc4fee02d51d -m my-marketplace-dev Seq ID Resource ID Event type Created at local time Source Actor 3471856 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/created 2020-12-07 9:47:19 PM marketplace-api joe@example.com 3471857 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:28 PM marketplace-api joe@example.com 3471858 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:38 PM marketplace-api joe@example.com 3471859 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:53 PM marketplace-api joe@example.com 3471860 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:56 PM marketplace-api joe@example.com 3471863 5fce8728-0f15-46e4-8ee2-c4955e0cc079 availabilityException/created 2020-12-07 9:48:56 PM marketplace-api joe@example.com 3471864 5fce872f-4081-4ca7-a40d-6eb2b003dcc2 availabilityException/created 2020-12-07 9:49:03 PM marketplace-api joe@example.com 3471865 5fce8730-c750-4427-bc58-09f685cc0b03 availabilityException/created 2020-12-07 9:49:04 PM marketplace-api joe@example.com 3471866 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:49:11 PM marketplace-api joe@example.com 3471873 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:50:35 PM marketplace-api joe@example.com 3471890 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:53:15 PM marketplace-api joe@example.com ``` In this case the resource ID is again a listing ID. The returned events will include events about the listing itself as well as any other event that is for a resource that has a relationship to the given listing (such as availability exceptions, booking, transactions, and so on). This is useful when trying to investigate the change history of a listing's availability, the history of transactions for a given listing or user, and so on. Sharetribe CLI supports two different ways to look at events from the past. We can either use the sequence ID to define a query range or we can use timestamps matching event's created at. Sequence IDs are unique and strictly increasing, meaning that events that occurred later in time always have a larger sequence ID. With both options we can query either events after a known sequence ID / time or events right before a sequence id / time. ```bash $ flex-cli events --after-seqid 3391593 -M my-marketplace-dev # ... $ flex-cli events --before-seqid 3462912 -m my-marketplace-dev # ... $ flex-cli events --after-ts 2020-12-05 -m my-marketplace-dev # ... $ flex-cli events --before-ts 2020-12-05T10:00.000Z -m my-marketplace-dev ``` Timestamps can be given with date only or with the time component included using the ISO 8601 standard format. If the time component is omitted, it defaults to UTC midnight. Ranges are exclusive meaning that event with sequence ID 3391593 is not included in the output produced by the first command above but instead the output starts with the first event after that sequence ID. The `--filter`, `--resource` and `--limit` parameters can also be combined with time range queries. For example, we can get the next 100 user and listing events since 7th of December 2020 with the command: ```bash $ flex-cli events --after-ts 2020-12-07 --filter user,listing -m my-marketplace-dev ``` ## Output modes and examining events in detail In addition to the default table output mode, Sharetribe CLI supports `--json` and `--json-pretty` modes. In the table output mode the CLI prints only summary information about the events. This is useful when wanting to see the big picture, which events happened and when. When you want to look into the details of an event, you need to use a json mode. With the filtering parameter `--seqid` we can target a single event: ```bash $ flex-cli events --seqid 3471843 -m my-marketplace-dev --json-pretty { "eventType": "user/updated", "createdAt": "2020-12-07T19:45:38.721Z", "resourceType": "user", "source": "source/marketplace-api", "resourceId": "5fce8536-61f5-4c85-8160-61b1799d256f", "id": "062569cb-dc3e-5a80-bde5-bb966072ecd6", "resource": { "deleted": false, "banned": false, "email": "joe@example.com", "profileImage": { "id": "5fce865b-dbc7-432a-85cc-ddb3a993eede" }, "stripeConnected": false, "createdAt": "2020-12-07T19:40:38.902Z", "identityProviders": [], "pendingEmail": null, "emailVerified": true, "stripeAccount": null, "id": "5fce8536-61f5-4c85-8160-61b1799d256f", "marketplace": { "id": "5b83f0af-ed76-4fbf-9e71-e76b76c5abce" }, "profile": { "displayName": "olli-test F", "firstName": "olli-test", "privateData": {}, "protectedData": {}, "bio": "Test users for event demo", "abbreviatedName": "oF", "lastName": "Foobar5", "publicData": {}, "metadata": {} } }, "auditData": { "userId": "5fce8536-61f5-4c85-8160-61b1799d256f", "adminId": null, "clientId": "039f4354-ccfa-4677-9395-2bf3f2294355", "requestId": "b43fddd6-f893-4a69-8661-ef159013019b" }, "sequenceId": 3471843, "previousValues": { "profile": { "bio": null, "firstName": "Olli", "displayName": "Olli F", "abbreviatedName": "OF" }, "profileImage": null }, "marketplaceId": "5b83f0af-ed76-4fbf-9e71-e76b76c5abce" } ``` The event details reveal that this user update changed the `bio`, `firstName`, `displayName` and `abbreviatedName` for a user. For more information about how `previousValues` records changes, see event reference for [resource data and previous values](/references/events/#resource-data-and-previous-values). While `--json-pretty` is a good mode for examining the event details manually, `--json` is the mode you want when needing to programmatically parse the output. In `--json` mode Sharetribe CLI prints the events as one line JSON objects. ```bash $ flex-cli events -m my-marketplace-dev --json | less {"eventType":"availabilityException/created","createdAt":"2020-12-04T11:32:43.794Z","resourceType":"a {"eventType":"availabilityException/created","createdAt":"2020-12-04T11:32:44.346Z","resourceType":"a {"eventType":"availabilityException/created","createdAt":"2020-12-04T11:32:45.524Z","resourceType":"a {"eventType":"availabilityException/created","createdAt":"2020-12-04T11:32:49.604Z","resourceType":"a {"eventType":"availabilityException/deleted","createdAt":"2020-12-04T11:32:50.136Z","resourceType":"a {"eventType":"user/updated","createdAt":"2020-12-07T13:15:42.313Z","resourceType":"user","source":"so {"eventType":"user/updated","createdAt":"2020-12-07T13:17:30.935Z","resourceType":"user","source":"so {"eventType":"user/updated","createdAt":"2020-12-07T13:36:28.469Z","resourceType":"user","source":"so {"eventType":"availabilityException/created","createdAt":"2020-12-07T14:27:31.243Z","resourceType":"a {"eventType":"availabilityException/created","createdAt":"2020-12-07T14:27:31.835Z","resourceType":"a # ... ``` The output can be easily parsed and transformed by using e.g. the wonderful [jq](https://stedolan.github.io/jq/) command-line tool. ```bash $ flex-cli events --filter listing -m my-marketplace-dev --json | jq '{sequenceId, eventType, source, changedKeys: (.previousValues // {} | keys)}' { "sequenceId": 3471856, "eventType": "listing/created", "source": "source/marketplace-api", "changedKeys": [] } { "sequenceId": 3471857, "eventType": "listing/updated", "source": "source/marketplace-api", "changedKeys": [ "publicData" ] } { "sequenceId": 3471858, "eventType": "listing/updated", "source": "source/marketplace-api", "changedKeys": [ "publicData" ] } { "sequenceId": 3471859, "eventType": "listing/updated", "source": "source/marketplace-api", "changedKeys": [ "geolocation", "publicData" ] } { "sequenceId": 3471860, "eventType": "listing/updated", "source": "source/marketplace-api", "changedKeys": [ "price" ] } { "sequenceId": 3471866, "eventType": "listing/updated", "source": "source/marketplace-api", "changedKeys": [ "availabilityPlan" ] } { "sequenceId": 3471873, "eventType": "listing/updated", "source": "source/marketplace-api", "changedKeys": [ "images" ] } { "sequenceId": 3471890, "eventType": "listing/updated", "source": "source/marketplace-api", "changedKeys": [ "state" ] } ``` ## Live tailing events Sharetribe CLI also offers a way to follow events live as they happen, a.k.a. live tail them. The command to do this is `events tail` ```bash $ flex-cli help events tail Tail events live as they happen USAGE $ flex-cli events tail OPTIONS --filter=EVENT_TYPES Show only events of given types, e.g. '--filter listing/updated,user'. --json Print full event data as one JSON string. --json-pretty Print full event data as indented multi-line JSON string. --resource=RESOURCE_ID Show events for a specific resource ID only. -l, --limit=NUMBER Show given number of latest events and then start tailing (default is 10 and max is 100). Can be combined with other parameters. -m, --marketplace=MARKETPLACE_ID marketplace identifier ``` Live tailing accepts the same filtering parameters as the events query, `--filter` and `--resource`. You can also pass in `--limit` to control how many events from the past are shown before starting the live tail. Live tailing supports all the same output modes (default table output, `--json` and `--json-pretty`) as the events query does. ```bash $ flex-cli events tail -m my-marketplace-dev Starting live tail of events. Type +C to quit. Seq ID Resource ID Event type Created at local time Source Actor 3471858 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:38 PM marketplace-api joe@example.com 3471859 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:53 PM marketplace-api joe@example.com 3471860 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:47:56 PM marketplace-api joe@example.com 3471863 5fce8728-0f15-46e4-8ee2-c4955e0cc079 availabilityException/created 2020-12-07 9:48:56 PM marketplace-api joe@example.com 3471864 5fce872f-4081-4ca7-a40d-6eb2b003dcc2 availabilityException/created 2020-12-07 9:49:03 PM marketplace-api joe@example.com 3471865 5fce8730-c750-4427-bc58-09f685cc0b03 availabilityException/created 2020-12-07 9:49:04 PM marketplace-api joe@example.com 3471866 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:49:11 PM marketplace-api joe@example.com 3471873 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:50:35 PM marketplace-api joe@example.com 3471875 5fce8536-61f5-4c85-8160-61b1799d256f user/updated 2020-12-07 9:50:52 PM marketplace-api joe@example.com 3471890 5fce86c7-e435-4047-ab3b-dc4fee02d51d listing/updated 2020-12-07 9:53:15 PM marketplace-api joe@example.com ``` ## Summary Sharetribe CLI gives you visibility into the events that happen in your marketplace. It supports querying events in a few different ways based on event types, affected resource or time. It also supports following events that happen in your marketplace live. Using Sharetribe CLI to examine events is a great way to see what's happening in your marketplace and how the marketplace data has gotten to it's current state. Looking at the events using Sharetribe CLI is also a great way to get started with planning how to build an integration that reacts to events as it allows you to see what kind of events happen when, and what's the data that's recorded for each event. For more information about events, see the following resources: - [Events reference](/references/events/) for a detailed description on how events work and how they are defined. - [Integration API reference for events](https://www.sharetribe.com/api-reference/integration.html#events) about how to query events via the Integration API. - [Reacting to events](/how-to/events/reacting-to-events/) how-to guide guide --- ## How-tos Path: how-to/index.mdx # How-tos A collection of articles that help you to create custom features. ## Users and Authentication ## Listings ## Transaction process ## Payments ## Migrations ## Content management ## Emails and notifications ## Events ## Search --- ## Add buffer time to bookings in time-based listings Path: how-to/listings/bookings-with-buffer/index.mdx # Add buffer time to bookings in time-based listings For some services booked by the hour, the listing author may want to reserve a buffer time between bookings. For instance a bike rental agency may want to check their bikes between rentals, or a massage therapist may need to clear up and restock their therapy space before the next customer arrives. You can set your marketplace to add these kinds of buffers by default, so listing authors do not need to add availability exceptions manually. ## Add a 15 minute buffer between one hour bookings The simplest use case for buffered bookings is having one hour slots that are bookable at the top of the hour, and adding 15 minutes of non-bookable time after each slot. To achieve that, we will use the booking's display time attribute. First, we will add a helper function in `src/util/dates.js` file to add the correct buffer time. ```jsx filename="src/util/dates.js" const bufferMinutes = 15; export const addBuffer = (date) => moment(date).add(bufferMinutes, 'minutes').toDate(); ``` ### Add display end time handling The user can select one hour booking slots on the listing page. When an order gets initiated, we want to set the selected end time as the display time, and a new buffered end time as the actual booking time. This way, the booking extends over both the customer's booking time and the buffer, so that other customers can not book over the buffer. We use the newly created `addBuffer` function to extend the display end time. ```diff filename="src/containers/CheckoutPage/CheckoutPage.duck.js" + import { addBuffer } from '../../util/dates'; /* ... */ export const initiateOrder = (orderParams, transactionId) => (dispatch, getState, sdk) => { dispatch(initiateOrderRequest()); /* ... */ const quantityMaybe = quantity ? { stockReservationQuantity: quantity } : {}; - const bookingParamsMaybe = bookingDates || {}; + let bookingParamsMaybe = {}; + + if (bookingDates) { + bookingParamsMaybe = { + ...bookingDates, + bookingEnd: addBuffer(bookingDates.bookingEnd), + bookingDisplayEnd: bookingDates.bookingEnd, + bookingDisplayStart: bookingDates.bookingStart, + } + } // Parameters only for client app's server const orderData = deliveryMethod ? { deliveryMethod } : {}; /* ... */ ``` We also need to modify line item calculation. By default, the transaction's price is calculated based on the booking's start and end moments, however in this case we want to use the display end attribute for the price calculation. We have already passed `displayEnd` in `bookingData`, so we just need to add handling for it in `lineItems.js`. Since the function is used both with and without display end time, e.g. when calling `api/transaction-line-items`, we need to accommodate both use cases. The `lineItems.js` file has a helper function for calculating hourly booking quantity, so we can modify it directly. ```diff filename="src/api-util/lineItems.js" const getHourQuantityAndLineItems = orderData => { - const { bookingStart, bookingEnd } = orderData || {}; + const { bookingStart, bookingEnd, bookingDisplayEnd } = orderData || {}; + const end = bookingDisplayEnd ?? bookingEnd; const quantity = - bookingStart && bookingEnd ? calculateQuantityFromHours(bookingStart, bookingEnd) : null; + bookingStart && end ? calculateQuantityFromHours(bookingStart, end) : null; return { quantity, extraLineItems: [] }; }; ``` ### Set getStartHours to return correct available start times Finally, we need to make sure that the start time slots only show valid start times including the buffer – if someone has booked 6pm to 7pm, then another customer cannot book 5pm to 6pm because there is not enough availability for both the time slot and the buffer. First, add a constant for a full hour in addition to the buffer time. ```diff filename="src/util/dates.js" const bufferMinutes = 15; + const hourMinutes = 60; ``` Then, fix `getStartHours` handling. By default, start hours and end hours are determined from the same list, so `getStartHours` removes the final list item to allow for a single hour between the final start and end times. However, when the actual booking slot is longer than the displayed one, we need to make sure that the interval between the final start and end times can fit a buffered booking. Therefore, instead of removing a single start time to fit a one hour booking slot, we remove as many start times as it takes to fit a buffered hour before the final end time. ```diff filename="src/util/dates.js" export const getStartHours = (startTime, endTime, timeZone, intl) => { const hours = getSharpHours(startTime, endTime, timeZone, intl); - return hours.length < 2 ? hours : hours.slice(0, -1); + const removeCount = Math.ceil((hourMinutes + bufferMinutes) / hourMinutes); + return hours.length < removeCount ? [] : hours.slice(0, -removeCount); }; ``` ## Allow users to book buffered appointments at non-sharp hours Having this kind of setup then means that the slots are, in practice, bookable every two hours. If we do not want to leave extra gaps between bookings beyond the buffer, we can set start times to repeat on the buffer time cadence instead of hourly. That way, if someone books the 8 AM - 9 AM slot, the next available start time is 9.15 AM i.e. right after the buffer. In other words, we need to have start and end times at a different cycle: - start times at 15 minute increments i.e. a customer can start their booking at any quarter hour, instead of at the top of the hour only - end times at one hour increments, i.e. the customer can book one or more full hours only. Time slot handling is done using a few helper functions in src/util/dates.js - `getStartHours` and `getEndHours` return a list of timepoints that are displayed as the booking's possible start and end moments, respectively. They both use the same helper function `getSharpHours` - `getSharpHours` retrieves the sharp hours that exist within the availability time slot. It uses the `findBookingUnitBoundaries` function. - `findBookingUnitBoundaries` is a recursive function that checks whether the current boundary (e.g. sharp hour) passed to it falls within the availability time slot. - If the current boundary is within the availability time slot, the function calls itself with the next boundary and cumulates the boundary results into an array. - If the current boundary does not fall within the availability time slot, the function returns the cumulated results from the previous iterations. - `findBookingUnitBoundaries` takes a `nextBoundaryFn` parameter that it uses to determine the next boundary value to pass to itself. - the function passed as `nextBoundaryFn` by default is `findNextBoundary`. The `findNextBoundary` function increments the current boundary by a predefined value. ```js filename="src/util/dates.js" export const findNextBoundary = (timeZone, currentMomentOrDate) => moment(currentMomentOrDate) .clone() .tz(timeZone) .add(1, 'hour') // The default handling uses hours .startOf('hour') // By default, the time slot is rounded to the start of the hour .toDate(); ``` In addition to `findBookingUnitBoundaries`, the template uses `findNextBoundary` to handle other time increment boundaries. That is why, instead of modifying `findNextBoundary` directly, we will create a similar function called `findNextCustomBoundary` to be used in `findBookingUnitBoundaries`, so we do not need to worry about side effects. ### Add a custom rounding function for moment.js The template hourly listing handling uses the [moment-timezone](https://momentjs.com/timezone/) library to modify times and dates and convert them between the listing's time zone and the user's time zone. By default, the `findNextBoundary` function uses `moment.startOf('hour')` to round the booking slots to the top of each hour. For `findNextCustomBoundary` – since we are now dealing with minutes – we need to create a custom rounding function to replace the `startOf('hour')` function call. When we add it to `moment.js` using the prototype exposed through `moment.fn`, we can chain it in the same place as the default `startOf('hour')` function. This rounding function rounds to sharp hours when the buffer minutes value is a factor of an hour, e.g. 15, 20 or 30 minutes. ```jsx filename="src/util/dates.js" /** * Rounding function for moment.js. Rounds the Moment provided by the context * to the start of the specified time value in the specified units. * @param {*} value the rounding value * @param {*} timeUnit time units to specify the value * @returns Moment rounded to the start of the specified time value */ moment.fn.startOfDuration = function (value, timeUnit) { const getMs = (val, unit) => moment.duration(val, unit).asMilliseconds(); const ms = getMs(value, timeUnit); // Get UTC offset to account for potential time zone difference between // customer and listing const offsetMs = this._isUTC ? 0 : getMs(this.utcOffset(), 'minute'); return moment(Math.floor((this.valueOf() + offsetMs) / ms) * ms); }; ``` ### Add a custom boundary function We will create a new `findNextCustomBoundary` function to replace the default usage. We will use the new rounding function to replace the built-in `startOf()` function in our function. We also need to calculate the increment of time to add to each time boundary, i.e. how long are the stretches of time delineated by the boundaries. To do that, we need an `isStart` attribute, i.e. whether we're dealing with start times or end times. In addition we need an `isFirst` attribute indicating whether the boundary in question is the very first one in the list. Since we're rounding to the buffer time (here: 15 minutes), we'll need to manually set the first time slot to correspond to the start of the available time slot. ```jsx filename="src/util/dates.js" export const findNextCustomBoundary = ( currentMomentOrDate, timeUnit, timeZone, isFirst, isStart ) => { // For end time slots (i.e. not start slots), add a full hour. // For the first start slot, use the actual start time. // For other start slots, use the buffer time. const increment = !isStart ? hourMinutes : isFirst ? 0 : bufferMinutes; return moment(currentMomentOrDate) .clone() .tz(timeZone) .add(increment, timeUnit) .startOfDuration(bufferMinutes, timeUnit) .toDate(); }; ``` ### Use new boundary function in helper functions The default `findNextBoundary` function is called from the `findBookingUnitBoundaries` function, so we need to replace it with the `findNextCustomBoundary` function and make sure the `isStart` and `isFirst` parameters are passed correctly. The function gets `findNextBoundary` function as the `nextBoundaryFn` parameter. ```diff filename="src/util/dates.js" const findBookingUnitBoundaries = params => { const { cumulatedResults, currentBoundary, startMoment, endMoment, nextBoundaryFn, intl, timeZone, + isStart, timeUnit = 'hour', } = params; if (moment(currentBoundary).isBetween(startMoment, endMoment, null, '[]')) { const timeOfDay = formatDateIntoPartials(currentBoundary, intl, { timeZone })?.time; + // The nextBoundaryFn by definition cannot determine the first timepoint, since it + // is always based on a previous boundary, we pass 'false' as the 'isFirst' param + const isFirst = false; // Choose the previous (aka first) sharp hour boundary, // if daylight saving time (DST) creates the same time of day two times. const newBoundary = cumulatedResults && cumulatedResults.length > 0 && cumulatedResults.slice(-1)[0].timeOfDay === timeOfDay ? [] : [ { timestamp: currentBoundary.valueOf(), timeOfDay, }, ]; return findBookingUnitBoundaries({ ...params, cumulatedResults: [...cumulatedResults, ...newBoundary], - currentBoundary: moment(nextBoundaryFn(currentBoundary, timeUnit, timeZone)), + currentBoundary: moment(nextBoundaryFn(currentBoundary, timeUnit, timeZone, isFirst, isStart)), }); } return cumulatedResults; }; ``` The `findBookingUnitBoundaries`, in turn, is called from `getSharpHours`. We need to use `findNextCustomBoundary` for `findBookingUnitBoundaries`, pass the `isStart` and `isFirst` parameters with the first currentBoundary definition, as well as add the `isStart` parameter to the `findBookingUnitBoundaries` function call. In addition, we need to use the actual start time instead of the one millisecond before, which is used by default. This is necessary because instead of adding an hour and rounding off an hour as in the default implementation, we are now manually setting the start time to the beginning of the available time slot. ```diff filename="src/util/dates.js" - export const getSharpHours = (startTime, endTime, timeZone, intl) => { + export const getSharpHours = (startTime, endTime, timeZone, intl, isStart = false) => { if (!moment.tz.zone(timeZone)) { throw new Error( 'Time zones are not loaded into moment-timezone. "getSharpHours" function uses time zones.' ); } + const isFirst = true; + // Select a moment before startTime to find next possible sharp hour. // I.e. startTime might be a sharp hour. const millisecondBeforeStartTime = new Date(startTime.getTime() - 1); return findBookingUnitBoundaries({ - currentBoundary: findNextBoundary(millisecondBeforeStartTime, 'hour', timeZone), + currentBoundary: findNextCustomBoundary(startTime, 'minute', timeZone, isFirst, isStart), startMoment: moment(startTime), endMoment: moment(endTime), - nextBoundaryFn: findNextBoundary, + nextBoundaryFn: findNextCustomBoundary, cumulatedResults: [], intl, timeZone, - timeUnit: 'hour', + isStart, + timeUnit: 'minutes', }); }; ``` ### Fix getStartHours and getEndHours handling To get correct start times, we need to first pass `true` as the `isStart` parameter from `getStartHours` to `getSharpHours`. In addition, we again need to make sure that even when selecting the last start time, there is enough availability for the first timeslot. Since the first time slots are now set at the buffer minute interval, we divide the full booking time by `bufferMinutes` to get the correct `removeCount` value. ```diff filename="src/util/dates.js" export const getStartHours = (startTime, endTime, timeZone, intl) => { - const hours = getSharpHours(startTime, endTime, timeZone, intl); - const removeCount = Math.ceil((hourMinutes + bufferMinutes) / hourMinutes) + const hours = getSharpHours(startTime, endTime, timeZone, intl, true); + const removeCount = Math.ceil((hourMinutes + bufferMinutes) / bufferMinutes) return hours.length < removeCount ? [] : hours.slice(0, -removeCount); }; ``` Finally, we can simplify the end hour handling. Since the first entry is determined in the `findNextBoundary` function, we do not need to remove it. Instead, we can just return the full list from `getSharpHours`. ```diff filename="src/util/dates.js" export const getEndHours = (intl, timeZone, startTime, endTime) => { - const hours = getSharpHours(startTime, endTime, timeZone, intl); - return hours.length < 2 ? [] : hours.slice(1); + return getSharpHours(startTime, endTime, timeZone, intl); }; ``` Now, if we have a booking from 9 AM to 10 AM with a 15 minute buffer at the end, the next customer can start their booking at 10:15 AM. Conversely, the previous booking can begin 7:45 AM and no later, so that the buffered time slot can fit in before the already booked session. ![Booking start options buffered time slots](./quarter_hour_starts.png) --- ## Extend listing data in Sharetribe Web Template Path: how-to/listings/extend-listing-data-in-template/index.mdx # Extend listing data in Sharetribe Web Template This guide shows you a code-based approach to expanding the listing data model in your marketplace. We'll have a look on how the listing can be configured so that the data gets added, and how it can then be presented and used to filter searches. Adding new attributes to the data model relies on [extended data](/references/extended-data/). In Sharetribe Web Template, top-level listing extended data is configured in the [configListing.js](https://github.com/sharetribe/web-template/blob/main/src/config/configListing.js) file. Settings configured in local configurations files are overridden by any fetched via the Asset Delivery API. You can refer to [this article](/template/configuration/hosted-and-local-configurations/) to modify the way your template merges local and hosted configurations. Configuring the listing data this way allows you to - declare the attribute and its possible values - show the attribute selection inputs in the listing editing wizard - optionally show the attribute values on the listing page and - optionally use the attribute as a search filter. There are a handful of use cases that are possible to achieve with code-based configurations but not Console-based ones. These include determining the attribute's search mode to **has_any**, and setting a default value for the attribute's search schema. If you want to configure a complex extended data attribute, e.g. a JSON object, it is good to note that Sharetribe only allows searching and filtering top-level attributes. In other words, complex attributes cannot be used to filter listings. However, they can be useful in storing other relevant listing data. For a more complex attribute, you will need to add extended data directly to a listing. You will also need to make custom changes to your listing page, if you want to show the attribute there. ## Define a has_any multi-enum field for listings In this article, we will extend top-level data using **configListing.js** to use a **has_any** search pattern. When configuring a multi-enum field in Console, the search is always in **has_all** mode – selecting two options returns listings with both of those attributes selected. In the Marketplace API, it is also possible to search with **has_any** mode – selecting two options returns listings with either of the options. Let's extend the default bike related listing data by adding an attribute 'accessories' to show what accessories are included. The full configuration looks like this: ```jsx filename="configListing.js" { key: 'accessories', scope: 'public', schemaType: 'multi-enum', enumOptions: [ { option: 'bell', label: 'Bell' }, { option: 'lights', label: 'Lights' }, { option: 'lock', label: 'Lock' }, { option: 'mudguard', label: 'Mudguard' }, ], saveConfig: { label: 'Accessories', placeholderMessage: 'Select an option…', isRequired: false, }, filterConfig: { showFilter: true, label: 'Accessories', searchMode: 'has_any', group: 'secondary', }, showConfig: { label: 'Accessories', }, }, ``` ### Declare the attribute and its possible values Extended data attributes in the `configListing.js` file need to be defined, at minimum, by **key**, by **scope**, and by **schemaType**. ```jsx filename="configListing.js" key: 'accessories', scope: 'public', schemaType: 'multi-enum', enumOptions: [ { option: 'bell', label: 'Bell' }, { option: 'lights', label: 'Lights' }, { option: 'lock', label: 'Lock' }, { option: 'mudguard', label: 'Mudguard' }, ], // If you have multiple listing types, you can define the types that should have this field // listingTypeConfig: { // limitToListingTypeIds: true, // listingTypeIds: [ // 'selling' // ] // } ``` This attribute is defined as **public**, so it will be saved into the listing as **publicData.accessories**. The **schemaType** attribute determines the shape of the data being saved: - **enum** attributes are saved as a single string value from a list of predefined options - **multi-enum** attributes are saved as an array of string values from a list of predefined options - **boolean** attributes are saved as **true** or **false** boolean values - **long** attributes are saved as long i.e. as an 8-byte whole number - **shortText** attributes are saved as a single short-text entry e.g. a URL - **text** attributes are saved as a single text entry If the schema type is **enum** or **multi-enum**, you will need to define an array of **enumOptions** for the attribute. This allows the listing editing wizard to show the options when your user creates the listing, and it also provides the options for the search filters. If your marketplace uses multiple listing types, you can optionally define an attribute **listingTypeConfig**. This object should have two attributes: - **limitToListingTypeIds**, a boolean attribute determining whether to limit the attribute to specific listing types - **listingTypeIds** an array of the listing types that should have this field. If this attribute is not set, then the field is included for all listing types. Note that if you limit a listing field to specific listing types, the filters for that field will only show up on the search page if the listing type in question is selected as a filtering parameter. If you want a filter to always show up on the search page, don't limit it to listing types. ### Configure the listing detail editing page The `EditListingDetailsPanel` is configured to show specific inputs for specific schema types. This means that you only need to configure how the attribute shows up in the panel. You can separately determine the label for edit listing page and the other contexts where the attribute shows up. You can also set the attribute as required, and determine the error message to show if the attribute is missing. ```jsx filename="configListing.js" saveConfig: { label: 'Accessories', placeholderMessage: 'Select an option…', isRequired: false, }, ``` ### Configure search Top-level attributes can be set as searchable, but you might have listing attributes you do not want to use for filtering listings. For instance, you may have private data text fields that the listing author can use for listing-specific notes. For searchable attributes, you will need to include the **filterConfig** attribute to your listing configuration. In addition, you will need to [define a search schema](/how-to/search/manage-search-schemas-with-sharetribe-cli/). Make sure you define the search schema **type** according to the listing configuration **schemaType**. For multi-enum attributes, you can use **searchMode** to define whether you want to show - listings with all the query attributes (_has_all_), or - listings with any of the query attributes(_has_any_). If searchMode is not defined, or if you define a listing field in Console, the default is _has_all_. To define a multi-enum listing field with _has_any_ search logic, you will need to define the field in your local code. ```jsx filename="configListing.js" filterConfig: { showFilter: true, label: 'Accessories', searchMode: 'has_any', group: 'secondary', }, ``` When you define your listing field in code, you will need to [add a search schema](/how-to/search/manage-search-schemas-with-sharetribe-cli/#adding-listing-search-schemas) for the attribute to enable it for search. ### Configure the listing page The configuration for showing top-level extended data on the listing page is straightforward. In addition to the label, you can determine whether to show specific attribute values on the listing page. By default, all listing config attributes with a **showConfig.label** are shown on the listing page, but by setting **isDetail** to **false** on an attribute with schema type _enum_, _long_, or _boolean_, you can hide the attribute from the Details section on the listing page. ```jsx filename="configListing.js" showConfig: { label: 'Accessories', }, ``` And that is it! With this configuration, the attribute can be added to the listing, used for search, and shown on the listing page. If there are existing listings, they don't get an update before their extended data is updated by the listing author. (Operator can also do this one-by-one through Console or through Integration API.) So, you should assume that there are listings without these new extended data fields and, therefore, there should be some safeguards against undefined values. --- ## Modify booking time intervals in time-based listings Path: how-to/listings/modify-time-intervals/index.mdx # Modify booking time intervals in time-based listings In Sharetribe, listings can have either day-based or time-based availability. For listings with time-based availability, the available [time slots](https://www.sharetribe.com/api-reference/marketplace.html#time-slots) are returned from the API as continuous stretches of time. The client application must therefore split the availability into suitable booking lengths. The default behavior of the Sharetribe Web Template hourly listings is to split the continuous availability stretch into one hour bookable intervals. This how-to guide illustrates a handful of different cases on modifying booking lengths. These changes apply to all listings where the unit type is set to "hour". Starting with Sharetribe Web Template [v8.0.0](https://github.com/sharetribe/web-template/releases/tag/v8.0.0), the template also supports fixed timeslots, where the provider can choose the length of possible bookings, and the customer only selects a start time for their booking. This guide applies to non-fixed time slots, where the customer can select multiple intervals of the available booking length by setting a start time and an end time. ## Set booking length to 30 minutes The simplest use case is to create uniform 30 minute booking slots. Start with adding a constant for the booking length in minutes. ```jsx filename="src/util/dates.js" const timeSlotMinutes = 30; ``` Time slot handling is done using a few helper functions in `src/util/dates.js` - `getStartHours` and `getEndHours` return a list of timepoints that are displayed as the booking's possible start and end moments, respectively. They both use the same helper function `getSharpHours` - `getSharpHours` retrieves the sharp hours that exist within the availability time slot. It uses the `getBoundaries` function and passes in hard-coded 1 hour time increments. - The `getBoundaries` function fetches the boundaries for bookable slots based on a given time unit. It calls the `findBookingUnitBoundaries` function. - `findBookingUnitBoundaries` is a recursive function that checks whether the current boundary (e.g. sharp hour or custom time span) passed to it falls within the availability time slot. - If the current boundary is within the availability time slot, the function calls itself with the next boundary and cumulates the boundary results into an array. - If the current boundary does not fall within the availability time slot, the function returns the cumulated results from the previous iterations. - `findBookingUnitBoundaries` takes a `nextBoundaryFn` parameter that it uses to determine the next boundary value to pass to itself. - the function passed as `nextBoundaryFn` is `findNextBoundary`. The `findNextBoundary` function increments the current boundary by a predefined value, both for custom time units and for the default ones – day and hour. ```jsx filename="src/util/dates.js" showLineNumbers{718} export const findNextBoundary = ( currentDate, unitCount, timeUnit, timeZone ) => { const customTimeUnitConfig = bookingTimeUnits[timeUnit]?.isCustom ? bookingTimeUnits[timeUnit] : null; if (!!customTimeUnitConfig) { // If the time unit is custom, we need to use startOfMinuteBasedInterval function to adjust 00, 15, 30, 45 rounding. const customTimeUnitInMinutes = customTimeUnitConfig?.timeUnitInMinutes; const minuteOffset = !!customTimeUnitInMinutes ? unitCount * customTimeUnitInMinutes : unitCount; return moment(currentDate) .clone() .tz(timeZone) .add(minuteOffset, 'minute') .startOfMinuteBasedInterval(customTimeUnitInMinutes) .toDate(); } else { // Other time units are handled with the default moment.js functions return moment(currentDate) .clone() .tz(timeZone) .add(unitCount, timeUnit) .startOf(timeUnit) .toDate(); } }; ``` ### Use the custom extension function for moment.js The template hourly listing handling uses the [moment-timezone](https://momentjs.com/timezone/) library to modify times and dates and convert them between the listing's time zone and the user's time zone. By default, the `findNextBoundary` function uses `moment.startOf(timeUnit)` to round the booking slots to the top of each hour or day. For custom time slots, the function uses customized moment.js extension `startOfMinuteBasedInterval()`, which is defined earlier in the same `dates.js` file. ```jsx filename="src/util/dates.js" moment.fn.startOfMinuteBasedInterval = function (unitCount) { const durationInMs = moment .duration(unitCount, 'minutes') .asMilliseconds(); // Calculate the number of durations since 1970-01-01 00:00:00 const durationCount = Math.floor(this.valueOf() / durationInMs); // Return a moment that is rounded to the start of the previous whole number of durations return moment(durationCount * durationInMs); }; ``` It is added to `moment.js` using the prototype exposed through `moment.fn`, so that we can chain it in the same place as the default `startOf(timeUnit)` function. ### Add a new block for handling customized time slots You will need to create a new `else if` block for the hourly unit type, where you use the `startOfMinuteBasedInterval` function instead of the default `startOf(timeUnit)` function, and pass the `timeSlotMinutes` value as the addition and rounding duration values. ```jsx filename="src/util/dates.js" {25-32} export const findNextBoundary = ( currentDate, unitCount, timeUnit, timeZone ) => { const customTimeUnitConfig = bookingTimeUnits[timeUnit]?.isCustom ? bookingTimeUnits[timeUnit] : null; if (!!customTimeUnitConfig) { // If the time unit is custom, we need to use startOfMinuteBasedInterval function to adjust 00, 15, 30, 45 rounding. const customTimeUnitInMinutes = customTimeUnitConfig?.timeUnitInMinutes; const minuteOffset = !!customTimeUnitInMinutes ? unitCount * customTimeUnitInMinutes : unitCount; return moment(currentDate) .clone() .tz(timeZone) .add(minuteOffset, 'minute') .startOfMinuteBasedInterval(customTimeUnitInMinutes) .toDate(); } else if (timeUnit === 'hour') { // Replace hourly handling with the defined booking length return moment(currentDate) .clone() .tz(timeZone) .add(timeSlotMinutes, 'minute') .startOfMinuteBasedInterval(timeSlotMinutes) .toDate(); } else { // Other time units are handled with the default moment.js functions return moment(currentDate) .clone() .tz(timeZone) .add(unitCount, timeUnit) .startOf(timeUnit) .toDate(); } }; ``` For listings with an hourly price, the server-side function `calculateQuantityFromHours` in [server/api-util/lineItemHelpers.js]() determines the correct quantity as a decimal of full hours. However, if you want to set a price per minute, or e.g. a price per non-hour session, you will need to modify `calculateQuantityFromHours` in that file as well. ![Booking breakdown with half hour booking](./30_minute_booking.png) Note that this change also changes the time intervals available for modifying a listing's availability exceptions. If you want to continue handling availability exceptions with full hours, you can introduce a parameter `useFullHours = true` and pass it down the following path: - `getStartHours` to `getSharpHours` - `getEndHours` to `getSharpHours` - `getSharpHours` to `getBoundaries` - `getBoundaries` to `findBookingUnitBoundaries` and the `findNextBoundary` function defined as `currentBoundary` - `findBookingUnitBoundaries` to the `nextBoundaryFn` call defined as `currentBoundary` In `findNextBoundary`, replace the `else if (timeUnit === 'hour')` condition with `else if (!useFullHours)` Finally, you'll need to modify `FieldDateAndTimeInput.js` usages of `getStartHours` and `getEndHours` to pass `false` for the `useFullHours` parameter, so that they use your modified handling instead. ## Use an irregular time slot If your marketplace has custom booking lengths longer than (and not divisible by) 30 minutes, you will need to extend the previous steps to a more complex approach to make sure the time slots show up correctly. ### Find the rounding duration When the booking length is not a factor of a full hour, using the `timeslotMinutes` value might cause issues, because the start time slot gets rounded to a multiple of the time slot in general. This means that depending on the start time of the availability (8 AM vs 9 AM vs 10 AM), the first time slot may show up as starting 15 minutes or half hour past the actual desired start time. To align the first available boundary with a sharp hour, we need to manually set the first boundary to the specified start time, and set rounding to a factor of a full hour. To determine the correct rounding minute amount, we calculate the greatest common factor of the booking length and a full hour using the [Euclidean algorithm](https://en.wikipedia.org/wiki/Euclidean_algorithm). For instance, when using a 45 minute time slot, the greatest common divisor with an hour is 15 minutes. ```jsx filename="src/util/dates.js" const timeSlotMinutes = 45; const hourMinutes = 60; /** * Calculate the greatest common factor (gcf) of two timeslot lengths * to determine rounding value using the Euclidean algorithm * (https://en.wikipedia.org/wiki/Euclidean_algorithm). */ const gcf = (a, b) => { return a ? gcf(b % a, a) : b; }; /** * Define the rounding value. * If the first time slot is shorter than general time slot, * swap the parameters around so that the first parameter is the shorter one */ const rounding = gcf(timeSlotMinutes, hourMinutes); ``` ### Manually set first boundary to start time To manually set the first boundary to the start time, we need to pass an `isFirst` parameter to the `findNextBoundary` function. For the first time slot, we then skip incrementing completely. The rounding function now rounds the start time back to the rounding boundary. The default start time is passed to `findNextBoundary` as one millisecond before start time, since the default addition of 30 minutes and the `startOfDuration(...)` function cancel each other out. Now that we want to set the first booking slot manually, we also need to revert that millisecond change in our modified handling. ```diff filename="src/util/dates.js" - export const findNextBoundary = (currentDate, unitCount, timeUnit, timeZone) => { + export const findNextBoundary = (currentDate, unitCount, timeUnit, timeZone, isFirst = false) => { const customTimeUnitConfig = bookingTimeUnits[timeUnit]?.isCustom ? bookingTimeUnits[timeUnit] : null; /* ... */ } else if (timeUnit === 'hour') { + // Add separate handling for the first timeslot so that bookings can start + // from the beginning of the available time slot + const increment = isFirst ? 0 : timeSlotMinutes; + // Revert the millisecondBeforeStartTime change if increment is 0, so that the rounding + // works correctly + const date = isFirst ? new Date(currentDate.getTime() + 1) : currentDate; // Replace hourly handling with the defined booking length - return moment(currentDate) + return moment(date) .clone() .tz(timeZone) - .add(timeSlotMinutes, 'minutes') - .startOfMinuteBasedInterval(timeSlotMinutes) + .add(increment, 'minutes') + .startOfMinuteBasedInterval(rounding) .toDate(); } else { // Other time units are handled with the default moment.js functions return moment(currentDate) /* ... */ ``` Then, in the `getBoundaries` function that calls `findNextBoundary`, we will need to pass `true` as the `isFirst` parameter to the very first `findBookingUnitBoundaries` function call. ```diff filename="src/util/dates.js" export const getBoundaries = (startTime, endTime, unitCount, timeUnit, timeZone, intl) => { /* ... */ const millisecondBeforeStartTime = new Date(startTime.getTime() - 1); return findBookingUnitBoundaries({ + // Add isFirst param to determine first time slot handling - currentBoundary: findNextBoundary(millisecondBeforeStartTime, 1, timeUnit, timeZone), + currentBoundary: findNextBoundary(millisecondBeforeStartTime, 1, timeUnit, timeZone, true), startMoment: moment(startTime), endMoment: moment(endTime), /* ... */ ``` ## Add separate handling for first timeslot Sometimes, there are cases where you want to have a basic length for a booking and then different lengths for subsequent time slots. For instance, a listing could feature a 75 minute default bike rental with the option to extend it for 30 minutes at a time. In those cases, you need to create different handling for the first time slot, i.e. the first start and end boundaries. ```jsx filename="src/util/dates.js" const timeSlotMinutes = 30; const firstSlotMinutes = 75; /** * Define the rounding value. * If the first time slot is shorter than general time slot, * swap the parameters around so that the first parameter is the shorter one */ const rounding = gcf(timeSlotMinutes, firstSlotMinutes); ``` ### Determine first time slot boundaries In this use case, we want to determine a different behavior for the start and end boundaries of the first time slot. For this reason, we need to pass an `isStart` parameter to `findNextCustomBoundary` and use it to determine the boundary timepoints in addition to the `isFirst` parameter. ```diff filename="src/util/dates.js" export const findNextBoundary = ( currentDate, unitCount, timeUnit, timeZone, - isFirst = false + isFirst = false, + isStart = false, ) => { /* ... */ } else if (timeUnit === 'hour') { // Add separate handling for the first timeslot so that bookings can start // from the beginning of the available time slot + // Use the default booking length for non-first slots + // Use the first booking length for first end boundary + // Use 0 for first start boundary - const increment = isFirst ? 0 : timeSlotMinutes; + const increment = !isFirst + ? timeSlotMinutes + : !isStart + ? firstSlotMinutes + : 0; + // Revert the millisecondBeforeStartTime change if increment is 0, so that the rounding // works correctly const date = isFirst ? new Date(currentDate.getTime() + 1) : currentDate; /* ... */ }; ``` The `getBoundaries` and `getSharpHours` functions are used for both start hours and end hours, so we need to receive `isStart` as a parameter in both, and pass the value on to `findNextBoundary`. ```diff filename="src/util/dates.js" export const getBoundaries = ( startTime, endTime, unitCount, timeUnit, timeZone, intl, + isStart = false ) => { if (!moment.tz.zone(timeZone)) { /* ... */ return findBookingUnitBoundaries({ - // Add isFirst param to determine first time slot handling - currentBoundary: findNextBoundary(millisecondBeforeStartTime, 1, timeUnit, timeZone, true), + // Add isFirst and isStart params to determine first time slot handling + currentBoundary: findNextBoundary(millisecondBeforeStartTime, 1, timeUnit, timeZone, true, isStart), startMoment: moment(startTime), ``` ```diff filename="src/util/dates.js" - export const getSharpHours = (startTime, endTime, timeZone, intl) => { + export const getSharpHours = (startTime, endTime, timeZone, intl, isStart) => { - return getBoundaries(startTime, endTime, 1, 'hour', timeZone, intl); + return getBoundaries(startTime, endTime, 1, 'hour', timeZone, intl, isStart); }; ``` ### Customize start hour and end hour list behavior By default, `getStartHours` and `getEndHours` basically retrieve the same list, but `getStartHours` removes the last entry and `getEndHours` removes the first entry. Since we have custom start and end handling in `findNextBoundary`, we also need to modify the start and end hour lists. To get correct start times, we need to first pass `true` as the `isStart` parameter from `getStartHours` to `getSharpHours`. In addition, we need to make sure that even when selecting the last start time, there is enough availability for the first timeslot. We do this by removing enough entries from the end so that the first time slot can be booked even from the last start moment. ```diff filename="src/util/dates.js" export const getStartHours = (startTime, endTime, timeZone, intl) => { - const hours = getSharpHours(startTime, endTime, timeZone, intl); + const hours = getSharpHours(startTime, endTime, timeZone, intl, true); - return hours.length < 2 ? hours : hours.slice(0, -1); + // Remove enough start times so that the first slot length can successfully be + // booked also from the last start time + const removeCount = Math.ceil(firstSlotMinutes / timeSlotMinutes) + return hours.length < removeCount ? [] : hours.slice(0, -removeCount); }; ``` Finally, we can simplify the end hour handling. Since the first entry is determined in the `findNextCustomBoundary` function, we do not need to remove it. Instead, we can just return the full list from `getSharpHours`. ```diff filename="src/util/dates.js" export const getEndHours = (startTime, endTime, timeZone, intl) => { - const hours = getSharpHours(startTime, endTime, timeZone, intl); - return hours.length < 2 ? [] : hours.slice(1); + return getSharpHours(startTime, endTime, timeZone, intl); }; ``` We can then see that after the first booking length of 75 minutes, the subsequent boundaries are 30 minutes each. ![Booking end options for different first slot](./different_first_slot_booking.png) --- ## Migrating from outside Sharetribe ecosystem Description: How to import data from outside Sharetribe ecosystem Path: how-to/migrations/migrating-from-outside-sharetribe/index.mdx # Migrating from outside the Sharetribe ecosystem Data from an existing marketplace can be migrated to Sharetribe. If your marketplace is running in Sharetribe Go, there is a ready migration path that will be handled by us. We will handle the migration in this case and you can find the outline for this process [here](https://www.sharetribe.com/help/en/articles/8418385-migrating-from-sharetribe-go-to-the-new-sharetribe). If you are running a marketplace outside the Sharetribe ecosystem, and would like to import data to Sharetribe, it is possible. You will need to extract your data from its existing storage and transform it into Sharetribe's proprietary data format called Intermediary. Sharetribe will then validate the transformed data and load it into Sharetribe. This article will outline how you can encode data into Intermediary format, give you syntax examples of the format, and provide instructions on the migration process. ## When to request a migration? You should request a migration when: - You have a marketplace operating outside the Sharetribe ecosystem - You know you want to transfer your marketplace users and listings to your Sharetribe marketplace. If you’d prefer to re-start your community in Sharetribe, then a migration is not needed. - You have started developing with Sharetribe or configured your no-code settings to get started with a no-code Sharetribe marketplace. You will work with Sharetribe’s engineers to complete your migration. If you're planning to migrate your data to Sharetribe, you should always start the process with a test migration to your Dev or Test environment, and ensure everything looks correct there, before doing a live migration. If you want to initiate the test migration process, you should email hello@sharetribe.com with the subject “Migrating from outside Sharetribe”. Please include your Sharetribe marketplace ID from your [Console](https://console.sharetribe.com/) > Build > General > Marketplace name. ## Intermediary data Intermediary is an edn (https://github.com/edn-format/edn) data format that allows defining data rows for marketplaces. It supports creating links between rows via references. We recommend that you use an edn library to generate the data and validate the .edn syntax to ensure that the file is up to standard.
How to generate edn using a library? edn libraries exist for multiple languages, for example [JavaScript](https://www.npmjs.com/search?q=edn), [Python](https://pypi.org/search/?q=edn), and [Ruby](https://rubygems.org/search?query=edn). Clojure has built-in support for edn. Usually these libraries support encoding data structures to edn and creating custom tagged elements. Here's an example of how to write a bit of intermediary data in edn format using JavaScript libraries [jsedn](https://www.npmjs.com/package/jsedn) and [uuid](https://www.npmjs.com/package/uuid) to represent a user and a listing: ```javascript const edn = require('jsedn'); const { v4: uuidv4 } = require('uuid'); const tagged = (tag, value) => new edn.Tagged(new edn.Tag(tag), value); const uuid = () => tagged('uuid', uuidv4()); const price = (amount, currency) => tagged('im/money', [amount, currency]); const email = (data) => { const { emailAddress } = data; return new edn.Map([ edn.kw(':im.email/address'), emailAddress, edn.kw(':im.email/verified'), true, ]); }; const profile = (data) => { const { firstName, lastName } = data; return new edn.Map([ edn.kw(':im.userProfile/firstName'), firstName, edn.kw(':im.userProfile/lastName'), lastName, ]); }; const user = (data) => { const { alias, emailAddress, firstName, lastName } = data; const role = new edn.Vector([ edn.kw(':user.role/customer'), edn.kw(':user.role/provider'), ]); return new edn.Vector([ new edn.Vector([edn.kw(':im.user/id'), uuid(), alias]), new edn.Map([ edn.kw(':im.user/primaryEmail'), email(data), edn.kw(':im.user/createdAt'), tagged('inst', new Date().toISOString()), edn.kw(':im.user/role'), role, edn.kw(':im.user/profile'), profile(data), ]), ]); }; const listing = (data) => { const { alias, title, priceAmount, author } = data; return new edn.Vector([ new edn.Vector([edn.kw(':im.listing/id'), uuid(), alias]), new edn.Map([ edn.kw(':im.listing/title'), title, edn.kw(':im.listing/createdAt'), tagged('inst', new Date().toISOString()), edn.kw(':im.listing/state'), edn.kw(':listing.state/published'), edn.kw(':im.listing/price'), price(priceAmount, 'EUR'), edn.kw(':im.listing/author'), tagged('im/ref', author), ]), ]); }; const userAlias = edn.kw(':user/john'); const e = new edn.Map([ edn.kw(':ident'), edn.kw(':mymarketplace'), edn.kw(':data'), new edn.Vector([ listing({ alias: edn.kw(':listing/rock-sauna'), title: 'A solid rock sauna', priceAmount: 12.2, author: userAlias, }), user({ alias: userAlias, emailAddress: 'foo@sharetribe.com', firstName: 'John', lastName: 'Doe', }), ]), ]); console.log(edn.encode(e)); // or save to file ```
### Format Intermediary is specified as [a map](https://clojure.org/reference/data_structures#Maps) with the following keys: - :ident - identifies the target marketplace by marketplace ID, - :data - a [seq](https://clojure.org/reference/sequences) of marketplace data rows. Each row is a 2-tuple (an array with two elements) of id and content. You can find your marketplace ID in Sharetribe Console > Build > General. Note that the anonymised test file needs to specify your dev environment marketplace ID and your live data file needs to specify your live environment marketplace ID. The id part of the data row 2-tuple is specified as a tuple of 1 to 3 elements. The first element is always an id attribute and identifies the row type. The second element can be an alias or an import id. A 3 element version has id attr, import id and an alias. ```clj ;; 1 element tuple [:im.stripeAccount/id] ;; 2 element tuple with import id [:im.image/id #uuid "58afd8e1-e336-4ca4-a1e7-ff1d91856a6c"] ;; 2 element tuple with alias [:im.user/id :user/jane] ;; 3 element tuple [:im.listing/id #uuid "b074e697-ab0c-4746-a195-c58d73606b1f" :listing/rock-sauna] ``` Import ids can be given to rows and they should be unique for the marketplace and have type [UUID](#uuid). Aliases can be used to reference rows from other rows. Aliases are useful when the data is written by hand. Import ids are useful for programmatically generated / exported data. Note that import ids are only used in the import phase and they can not be mapped to existing resource ids, because the final resource id is not created based on the import id. In other words, if e.g. your dev marketplace already has data rows, you cannot reference those rows by If you are not generating data by hand, it is recommended that you use the 2 element form of the id tuple. This means using unique UUIDs throughout the file to reference entities. You can read more about [generating UUIDs for your data here](https://www.uuidgenerator.net/dev-corner). #### Aliases If you currently use non-UUID ids in your data, you can also use them to generate aliases for the data. Aliases must be unique across the file, and they cannot contain spaces. Since aliases are namespaced keywords (e.g. `:user/jane` in the example), the unique part cannot begin with a number. If your unique ids begin with a number, you can generate the aliases for instance in the following pattern: ```clj // type: 'user', id: 123 alias = (type, id) => // returns ':user/u123' return ':{type}/{type1stletter}{id}' ``` #### Referencing When you have data that is dependent on each other, such as listings related to users and reviews related to listings, you can reference the data within the import file. You can refer to resources by either alias or UUID with the `#im/ref` tagged element. The supported references for each type, if any, are listed in the type description in the [Supported types](#supported-types) section below. Note that referencing data with an id or alias that does not exist in the same file causes a validation error. ```clj ;; Referencing a listing by alias [[:im.image/id #uuid "58afd8e1-e336-4ca4-a1e7-ff1d91856a6c"] #:im.image{:url "https://asset-url.someservice.com/path/to/img1.jpg" :sortOrder 1 :listing #im/ref :listing/rock-sauna}] ;; Referencing a listing by UUID [[:im.image/id] #:im.image{:url "https://asset-url.someservice.com/path/to/img2.jpg" :sortOrder 2 :listing #im/ref [:im.listing/id #uuid "b074e697-ab0c-4746-a195-c58d73606b1f"]}] ``` See the [example](#an-example-of-intermediary-format) for more details. ### Supported types Currently, the Intermediary format supports the following top level types: ``` - :im.user/id - :im.listing/id - :im.image/id - :im.stripeAccount/id - :im.review/id ``` This means that the Intermediary supports importing: - [Users](#user) - [Listings](#listing) - [Profile images](#image) - [Listing images](#image) - [Stripe accounts](#stripe-account) - You need to use the same Stripe keys in Sharetribe as in your current service - [Reviews](#review) The migration process has some built-in validation for the types within an Intermediary file. Optional keys must have non-empty values, and if an optional key does not have a value, the key must be omitted for that resource. Empty strings are validated as empty values. For listings, users, and reviews, the corresponding Sharetribe API resource reference is linked under their type description. It is good to note that the shape of the Intermediary data corresponds to the API reference, however there may be differences in e.g. attribute naming. #### Listing Supported fields to migrate: [API reference listing resource](https://www.sharetribe.com/api-reference/marketplace.html#listing-resource-format). Required attributes - `:im.listing/createdAt` ([#inst](#timestamp)) - `:im.listing/title` (string) - `:im.listing/state` - accepted values for `:im.listing/state` - `:listing.state/published` - `:listing.state/closed` - `:listing.state/draft` - `:listing.state/pendingApproval` Optional attributes - only include the key if it has a non-empty value - `:im.listing/description` (string) - `:im.listing/location` ([#location](#location)) - `:im.listing/price` ([#money](#money)) - `:im.listing/currentStock` (non-negative integer) - `:im.listing/author` (#im/ref) - `:im.listing/publicData` (JSON) - `:im.listing/metadata` (JSON) Supported references - `#im.listing/author` -> `#im.user` Example syntax: ```clj [[:im.listing/id #uuid "b074e697-ab0c-4746-a195-c58d73606b1f" :listing/rock-sauna] #:im.listing{:createdAt #inst "2018-04-17T06:55:04.291-00:00" :title "A solid rock sauna" :description "A very nice solid rock sauna built solely of wood.\nHere's some more sensible stuff." :state :listing.state/pendingApproval :location #im/location [23.12 21.21] :price #im/money [12.12M "EUR"] :currentStock 5 :publicData {:categoryLevel1 "rock" :amenities ["sauna" "pool"]} :author #im/ref :user/john}] ``` #### User Supported fields to migrate: [API reference current user resource](https://www.sharetribe.com/api-reference/marketplace.html#currentuser-resource-format). Inside the user resource, the email resource differs a bit from the documentation. The email resource is referenced under `:primaryEmail` key like this: ```clj :primaryEmail {:im.email/address "foo@sharetribe.com" :im.email/verified true} ``` The Intermediary file validation also checks that the email address format is valid. At the moment, all users created within Sharetribe are defined with both `:user.role/customer` and `:user.role/provider`. The user roles cannot be configured after the import, so we recommend that you add both roles for all users and determine any distinction between user groups in your client application. Required attributes for `:im.user` - `:im.user/primaryEmail` - `:im.email/address` ([validated](https://www.regular-expressions.info/email.html) email string) - `:im.email/verified` (boolean) - `:im.user/role` (array) - accepted values for `:im.user/role` - `:user.role/customer` - `:user.role/provider` - `:im.user/createdAt` ([#inst](#timestamp)) - `:im.user/profile` Required attributes for `:im.user/profile` - `:im.userProfile/firstName` (string) - `:im.userProfile/lastName` (string) Optional attributes for `:im.user/profile` - only include the key if it has a non-empty value - `:im.userProfile/displayName` (string) - `:im.userProfile/bio` (string) - `:im.userProfile/publicData` (JSON) - `:im.userProfile/protectedData` (JSON) - `:im.userProfile/privateData` (JSON) - `:im.userProfile/metadata` (JSON) - `:im.userProfile/avatar` (#im/ref) Supported references - `#im.userProfile/avatar` -> `#im.image` Example syntax ```clj [[:im.user/id :user/john] #:im.user{:createdAt #inst "2018-04-17T06:55:04.291-00:00" :primaryEmail {:im.email/address "foo@sharetribe.com" :im.email/verified true} :profile {:im.userProfile/firstName "John" :im.userProfile/lastName "Doe" :im.userProfile/displayName "John D" :im.userProfile/bio "He's just a poor boy from a poor family.\nSpare him his life from this monstrosity." :im.userProfile/publicData { :premiumAccount true } :im.userProfile/avatar #im/ref :avatar/john} :role [:user.role/customer :user.role/provider]}] ``` #### Image Image urls need to be public and properly encoded URLs, so that we can access the imported images while importing the data. Required attributes - `:im.image/url` (URL-encoded string) Optional attributes - only include the key if it has a non-empty value - `:im.image/listing` (#im/ref) - `:im.image/sortOrder` (integer) Supported references - `#im.image/listing` -> `#im.listing` Note that for profile images, the reference is added in [user](#user) with `#im.userProfile/avatar` -> `#im.image` Example syntax ```clj [[:im.image/id #uuid "58afd8e1-e336-4ca4-a1e7-ff1d91856a6c"] #:im.image{:url "https://asset-url.someservice.com/path/to/img1.jpg" :sortOrder 1 :listing #im/ref :listing/rock-sauna}] ``` #### Stripe account Required attributes - `:im.stripeAccount/stripeAccountId` (string) - `:im.stripeAccount/user`(#im/ref) Example syntax ```clj [[:im.stripeAccount/id] #:im.stripeAccount{:stripeAccountId "a_stripe_id" :user #im/ref :user/john}] ``` #### Review Supported fields to migrate: [API reference review resource](https://www.sharetribe.com/api-reference/marketplace.html#review-resource-format) There are two types of reviews: `ofCustomer` and `ofProvider`. They differ in the reference to listing, as `ofProvider` reviews have a reference to a listing and `ofCustomer` don't. Required attributes - `:im.review/type` - accepted values for `:im.review/type` - `:review.type/ofCustomer` - `:review.type/ofProvider` - `:im.review/state` - accepted values for `:im.review/state` - `:review.state/pending` - `:review.state/public` - `:im.review/rating` (integer between 1-5) - `:im.review/createdAt` ([#inst](#timestamp)) - `:im.review/author` (#im/ref) - `:im.review/subject` (#im/ref) - `:im.review/content` (string) Optional attributes - only include the key if it has a non-empty value - `:im.review/listing` (#im/ref) Supported references - `#im.review/listing` -> `#im.listing` - `#im.review/author` -> `#im.user` - `#im.review/subject` -> `#im.user` Example syntax ```clj [[:im.review/id] #:im.review{:content "Exactly as advertised. Bummed this was a one time deal." :rating 5 :type :review.type/ofProvider :state :review.state/public :createdAt #inst "2018-01-06T00:10:10Z" :author #im/ref :user/jane :subject #im/ref :user/john :listing #im/ref :listing/rock-sauna}] ``` ### Value types Intermediary format uses some notable value types: - [Location](#location) - [Money](#money) - [UUID](#uuid) - [Timestamp](#timestamp) #### Location Location is an Intermediary-specific type specified as a 2-tuple of latitude and longitude. Both latitude and longitude are 32 bit floats. The `location` attribute is used in the Sharetribe Web Template to provide listing location coordinates through either Mapbox or Google maps. If your source data has addresses specified, and you would like to use them to determine coordinate information, you can use a 3rd party geocoding api such as [Mapbox](https://docs.mapbox.com/api/search/geocoding/) or [Google](https://developers.google.com/maps/documentation/geocoding/overview) in your data transformation setup. We recommend that you provide a valid location attribute if your marketplace uses maps in some way, whether or not you provide an address attribute in e.g. public data. ```clj #:im.listing{;;... :location #im/location [23.12 21.21] ;;... } ``` #### Money Money is an Intermediary-specific type and it is defined as [ amount, currency ]. Note that in the [Sharetribe API reference for listings](https://www.sharetribe.com/api-reference/marketplace.html#listing-resource-format), the `money` type differs somewhat from the Intermediary `#im/money` type – the `#im/money` type uses decimals for the amount whereas the API `money` type amount is given as an integer representing the currency's minor unit. ```clj #:im.listing{;;... :price #im/money [12.12M "EUR"] ;;... } ``` #### UUID A [Universally Unique identifier (UUID)](https://en.wikipedia.org/wiki/Universally_unique_identifier) can be used to identify resources within the migration file using .edn's built-in `#uuid` tagged element. If your data already uses UUIDs, make sure their format matches [the canonical representation](https://en.wikipedia.org/wiki/Universally_unique_identifier#Format). ```clj :im.image/id #uuid "58afd8e1-e336-4ca4-a1e7-ff1d91856a6c" ``` #### Timestamp Timestamp in .edn is given as an `#inst` tagged element. ```clj :createdAt #inst "2018-04-17T06:55:04.291-00:00" ``` ## Things to consider ### No updates are supported The import is loaded only once to the live environment and currently updates with multiple live data imports are not supported. ### Test import We can perform multiple imports to the dev environment, with the caveat that no data deletion in this situation is possible. This might lead to duplicate information being uploaded to the dev environment. Also, anonymizing test import data by hiding sensitive information like names, addresses, email addresses and Stripe keys is highly recommended. For anonymising email addresses, we recommend that you use + -aliases of an email address that you have access to (e.g. _admin+userId1@example.com_). This way, you can use the recover password feature to gain access to the individual accounts. Alternatively, you can use the Login as user feature to test anonymised user accounts. We recommend that you only use a subset of your data for the test import. The purpose of the test migration is to confirm that your extract-and-transform process creates a migration file that is valid and consistent, so there is rarely need for importing the anonymized equivalent of your full user data into the dev environment. ### Password management Importing passwords from outside the Sharetribe ecosystem is not supported. This means that your users will be logged out of the service and are required to use the recover password functionality to gain access. ### Security and sharing data Since you will be sharing user information about your service to ours, it is essential to share the data securely. Contact us when you are ready to share the data. Anonymised test data for validation and test migrations can be shared via email. However, for live data with real user information, we will create a secure upload link that is valid for an agreed amount of time. ## A full example of Intermediary format The following is a complete example demonstrating value types (#im/location, #im/money), import ids, aliases and referencing by import ids and aliases. ```clj {:ident :marketplace-ident :data [[[:im.listing/id #uuid "b074e697-ab0c-4746-a195-c58d73606b1f" :listing/rock-sauna] #:im.listing{:createdAt #inst "2018-04-17T06:55:04.291-00:00" :title "A solid rock sauna" :description "A very nice solid rock sauna built solely of wood.\nHere's some more sensible stuff." :state :listing.state/pendingApproval :location #im/location [23.12 21.21] :price #im/money [12.12M "EUR"] :currentStock 5 :publicData {:categoryLevel1 "rock" :amenities ["sauna" "pool"]} :author #im/ref :user/john}] [[:im.image/id #uuid "8bc7c21d-58f0-412d-8b89-be993893a356" :avatar/john] #:im.image{:url "https://asset-url.someservice.com/path/to/avatar.jpg"}] [[:im.user/id :user/john] #:im.user{:createdAt #inst "2018-04-17T06:55:04.291-00:00" :primaryEmail {:im.email/address "foo@sharetribe.com" :im.email/verified true} :profile {:im.userProfile/firstName "John" :im.userProfile/lastName "Doe" :im.userProfile/displayName "John D" :im.userProfile/bio "He's just a poor boy from a poor family.\nSpare him his life from this monstrosity." :im.userProfile/publicData { :premiumAccount true } :im.userProfile/avatar #im/ref :avatar/john} :role [:user.role/customer :user.role/provider]}] [[:im.stripeAccount/id] #:im.stripeAccount{:stripeAccountId "a_stripe_id" :user #im/ref :user/john}] [[:im.image/id #uuid "58afd8e1-e336-4ca4-a1e7-ff1d91856a6c"] #:im.image{:url "https://asset-url.someservice.com/path/to/img1.jpg" :sortOrder 1 :listing #im/ref :listing/rock-sauna}] [[:im.image/id] #:im.image{:url "https://asset-url.someservice.com/path/to/img2.jpg" :sortOrder 2 :listing #im/ref [:im.listing/id #uuid "b074e697-ab0c-4746-a195-c58d73606b1f"]}] [[:im.user/id :user/jane] #:im.user{:createdAt #inst "2018-04-17T06:55:04.291-00:00" :primaryEmail {:im.email/address "bar@sharetribe.com" :im.email/verified true} :profile {:im.userProfile/firstName "Jane" :im.userProfile/lastName "Doe" :im.userProfile/displayName "Jane D"} :role [:user.role/customer :user.role/provider]}] [[:im.review/id] #:im.review{:content "Exactly as advertised. Bummed this was a one time deal." :rating 5 :type :review.type/ofProvider :state :review.state/public :createdAt #inst "2018-01-06T00:10:10Z" :author #im/ref :user/jane :subject #im/ref :user/john :listing #im/ref :listing/rock-sauna}] [[:im.review/id] #:im.review{:content "Great customer!" :rating 5 :type :review.type/ofCustomer :state :review.state/public :createdAt #inst "2018-01-06T00:10:10Z" :author #im/ref :user/john :subject #im/ref :user/jane}]]} ``` --- ## How to customize pricing Path: how-to/payments/how-to-customize-pricing/index.mdx # How to customize pricing This how-to guide shows you how listing pricing can be customized by using two examples: adding an insurance fee to a bookable listing, and changing provider commission so that it's based on booking length. The changes we are about to make are as follows: {

} Update rental listing data model by storing insurance fee price in listing public data {

} Update pricing logic to add a insurance fee line item if a listing has insurance fee stored in public data {

} Update provider commission calculation to be dependent on booking length For more information about pricing in Sharetribe, see the [Pricing background article](/concepts/pricing-and-commissions/pricing/). ## Listing extended data Pricing can be based on a lot of variables, but one practical way to build it is to base it on information stored as extended data in listings. See the [Extend listing data in Sharetribe Web Template](/how-to/listings/extend-listing-data-in-template/) how-to guide to read how to extend the listing data model with extended data. See the same guide for instructions how to add inputs for new attributes in the listing wizard. Alternatively, in order to try out this guide, you can just add a hard-coded insurance fee to the `EditListingPricingPanel` component: Booking and product related transaction processes have different pricing panels, since the product process uses the `EditListingPricingAndStockPanel` component. This means that adding the insurance fee to the `EditListingPricingPanel` only adds it to bookable listings. However, we may want to set a different insurance fee for hourly listings and daily or nightly listings. On submit, save price and `insuranceFee`: ```diff filename="src/containers/EditListingPricingPanel/EditListingPricingPanel.js" + import { HOUR } from '../../../../transactions/transaction'; /* ... */ { const { price } = values; + const insuranceFeeAmount = unitType === HOUR ? 500 : 2000; + const insuranceFee = { amount: insuranceFeeAmount, currency: marketplaceCurrency }; /* ... */ if (unitType === FIXED || isPriceVariationsInUse) { /* ... */ updateValues = { ...priceVariantChanges, ...startTimeIntervalChanges, publicData: { priceVariationsEnabled: isPriceVariationsInUse, ...startTimeIntervalChanges.publicData, ...priceVariantChanges.publicData, + insuranceFee: insuranceFee, }, }; } else { - const priceVariationsEnabledMaybe = isBooking - ? { - publicData: { - priceVariationsEnabled: false, - }, - } - : {}; - updateValues = { price, ...priceVariationsEnabledMaybe }; + const priceVariationsEnabledMaybe = isBooking + ? { + priceVariationsEnabled: false, + } + : {}; + updateValues = {price, publicData: { ...priceVariationsEnabledMaybe, insuranceFee: insuranceFee, } }; } /> ``` ## Transaction line item for insurance fee As the previous section mentions, this guide expects that the insurance fee price is stored in listing public data in an object with two keys: `amount` and `currency`. The `amount` attribute holds the price in subunits whereas `currency` holds the currency code. For example, with a cleaning fee of $20 the subunit amount is 2000 cents. ```js filename="src/containers/EditListingPricingPanel/EditListingPricingPanel.js" const insuranceFeeAmount = unitType === HOUR ? 500 : 2000; /* ... */ publicData: { insuranceFee: { amount: insuranceFeeAmount, currency: marketplaceCurrency }, } ``` Sharetribe pricing uses [privileged transitions](/concepts/transactions/privileged-transitions/) to ensure flexible pricing models while keeping control of the pricing logic in a secure environment. Transitioning requests of privileged transitions are made from the server-side. Thus we'll need to update the pricing logic in the `/server/api-util/lineItems.js` file: In the helper function section of the file, add a function that resolves the insurance fee of a listing: ```jsx filename="server/api-util/lineItems.js" const resolveInsuranceFeePrice = (listing) => { const { amount, currency } = listing.attributes.publicData?.insuranceFee || {}; if (amount && currency) { return new Money(amount, currency); } return null; }; ``` Now the `transactionLineItems` function can be updated to also provide the insurance fee line item in case the listing has an insurance fee configured: ```jsx filename="server/api-util/lineItems.js" const insuranceFeePrice = resolveInsuranceFeePrice(listing); const insuranceFeeLineItem = insuranceFeePrice ? [ { code: 'line-item/insurance-fee', unitPrice: insuranceFeePrice, quantity: 1, includeFor: ['customer', 'provider'], }, ] : []; // Note: extraLineItems for product selling (aka shipping fee) // is not included in either customer or provider commission calculation. /* ... */ // Let's keep the base price (order) as first line item and provider and customer commissions as last. // Note: the order matters only if OrderBreakdown component doesn't recognize line-item. const lineItems = [ order, ...extraLineItems, ...insuranceFeeLineItem, ...getProviderCommissionMaybe( providerCommission, order, priceAttribute ), ...getCustomerCommissionMaybe( customerCommission, order, priceAttribute ), ]; ``` When selecting a code for your custom line-item, remember that Sharetribe requires the codes to be prefixed with _line-item/_ and the maximum length including the prefix is 64 characters. Other than that there are no restrictions, but we recommend that _kebab-case_ is used when the code consists of multiple words. Now, if you open up a bookable listing page and select dates in the order panel on the right, the template will fetch line items and you will see an insurance fee row in the order breakdown: ![Booking panel](./booking-panel.png) Note, that the order breakdown automatically renders the insurance fee line item by tokenizing the line item code and capitalizing the first letter. In case this is not enough, you can add your own presentational line item component to the booking breakdown. This is done by adding the line item code (in our case `line-item/insurance-fee`) into the `LINE_ITEMS` array in `src/util/types.js` and creating your own `LineItem*Maybe` component to be used in `OrderBreakdown`. ## Dynamic provider commission Now that we've updated the pricing logic based on listing extended data, let's next update the provider commission based on the booking length. The idea is to keep the 10% commission defined in Console for bookings of 5 or less nights. For bookings of more than 5 nights, we'll set the Update `lineItemHelpers.js` to add the new `calculateProviderCommissionPercentage` function and to update the `getProviderCommissionMaybe`: ```diff filename="server/api-util/lineItemHelpers.js" // Base provider and customer commissions are fetched from assets + const PROVIDER_COMMISSION_PERCENTAGE_REDUCTION = 3; + const calculateProviderCommissionPercentage = (order, providerCommission) => + order.quantity > 5 + ? providerCommission.percentage - PROVIDER_COMMISSION_PERCENTAGE_REDUCTION + : providerCommission.percentage; exports.getProviderCommissionMaybe = (providerCommission, order, priceAttribute) => { /* ... */ // The provider commission is what the provider pays for the transaction, and // it is the subtracted from the order price to get the provider payout: // orderPrice - providerCommission = providerPayout return useMinimumCommission ? [ { code: 'line-item/provider-commission', unitPrice: new Money(providerCommission?.minimum_amount, priceAttribute?.currency), quantity: getNegation(1), includeFor: ['provider'], }, ] : [ { code: 'line-item/provider-commission', unitPrice: totalMoneyIn, - percentage: getNegation(providerCommission.percentage), + percentage: getNegation(calculateProviderCommissionPercentage(order, providerCommission)), includeFor: ['provider'], }, ]; }; ``` Then update the `transactionLineItems` function in `lineItemHelpers.js` as follows: ```jsx filename="server/api-util/lineItemHelpers.js" const lineItems = [ order, ...extraLineItems, ...getProviderCommissionMaybe( providerCommission, order, priceAttribute, true ), ...getCustomerCommissionMaybe( customerCommission, order, priceAttribute ), ]; ``` Now when the provider takes a look at a pricing breakdown of a booking longer than 5 nights, the commission is calculated with 7% instead of 10%: ![Provider breakdown](./provider-breakdown.png) --- ## how-to/payments/how-to-integrate-3rd-party-payment-gateway/index.mdx Path: how-to/payments/how-to-integrate-3rd-party-payment-gateway/index.mdx # How to integrate a 3rd-party payment gateway with your marketplace Sharetribe provides out-of-the-box integration with Stripe. To reach markets not supported by Stripe, relying on another payment gateway can be crucial. This guide describes on a high-level, without going into the details of specific payment gateways, how to integrate any 3rd-party payment gateway (such as [PayPal Commerce Platform][paypal-commerce-platform], [MANGOPAY][mangopay-marketplaces], or [Adyen for Platforms][adyen-for-platforms]) with Sharetribe. ## Prerequisites Before reading this guide, you should be familiar with the following Sharetribe features: - [Privileged transitions][dev-docs-concepts-privileged-transitions] - [Events][dev-docs-refence-events] - [Reacting to events][dev-docs-howto-reacting-to-events] - [Extended data][dev-docs-reference-extended-data] ## Marketplace payment flow In this section we illustrate a marketplace payment flow in high-level and briefly discuss each stage. Later in this article, we'll go through how to integrate each state with your Sharetribe-powered marketplace. In a nutshell, a payment flow in a marketplace contains five significant steps. The following diagram illustrates a timeline of these steps: ![Marketplace payment flow](./payment-flow.png) {/* Diagram source: https://whimsical.com/how-to-integrate-a-3rd-party-payment-gateway-PBY6qRjauyb7v5pEdXY4pS */} {

Provider onboarding

} In this step, the provider connects their Sharetribe account with the payment gateway. This is the step when they provide the bank details where the money from the customers will be transferred to. In addition, in this step, they provide the necessary information and documents for the identity verification and _Know Your Customer (KYC)_ requirements. {

Customer checkout

} Customer checkout happens when the customer initiates the payment for a transaction. At this stage, they also provide the payment information, such as their credit card number. Also, the payment will be made at this point. The payment gateway will preauthorize the money, i.e. reserve the money on customer's credit card. {

Provider accept

} After the customer has checked out, the provider has ability to either accept or reject the request. If the request is accepted, the payment will be captured, and the reserved money will be transferred from customer's credit card to the payment gateway. Provider accept is a step that you can combine with the customer checkout. The flow where provider accept happens instantly after customer checkout is called "instant booking" flow. {

Customer refund

} Typically, the marketplace payment flow contains a delayed payment period. This is the time between the money is captured from customer's credit card and transferred to the provider's bank account. The payout in marketplaces usually happen after the provider has successfully provided the agreed service. Customer refund usually happens during the delayed payment period. There are many reasons why a refund may be necessary, for example, the provider or customer may not be able to make it or the provided service was not what was agreed. {

Provider payout

} If everything in the transaction went right and the customer received the agreed service, the money from the payment gateway will be eventually paid out to the provider. ## Can I just accept all payments to my own bank account and pay my providers manually? You might be wondering if it would be easier for you to just accept the entire payment to your own bank account, and handle refunds to customers payouts to providers separately, outside the main platform functionality. In terms of development work required to build the integration, this is indeed a lot easier. Any online payment service provider in the world supports such a simple checkout flow. However, this can lead to a myriad of issues in terms of regulation, accounting, and liability. You could be considered as holding other people's money, which is a heavily regulated area. In many countries, you need to acquire an expensive license for this purpose, and holding money without such a license is considered a crime. You might also be considered to be responsible for providing the goods or services your providers are selling, which is not always desirable. Finally, handling the payouts to the providers correctly can be quite a lot of manual, error-prone labor. You also still need to ensure the Know Your Customer process has been done correctly for them. Because of these challenges, we recommend you to only consider building a flow where the entire payment goes to your account only if you're absolutely sure that you're aware of all the consequences, and have gotten your approach greenlighted by your accountant and a lawyer familiar with the matter. From now on, this article focuses on integrating with a marketplace-specific payment solution, which handles the issues described above for you. [Learn more about choosing the right payment service provider][academy-payment-service-providers]. ## Before you start the integration Before you start coding the integration, we strongly advise you to contact the payment provider's customer support team. Keep in mind that while many payment providers give you access to their sandbox environment, access to the live environment usually needs contacting customer support and possibly signing a contract with them. Contact the support and make sure that: - They are available in the country where you operate your business - They support a marketplace-specific payment flow, handling Know Your Customer process for your providers and splitting payments - They can process the currencies of your marketplace - They can do payouts to your providers' country/countries - You are eligible to get access to the live environment - You know the process of how to get access to the live environment - You are familiar with the fees involved. ## White-label or hosted onboarding and payments Before integrating a 3rd-party payment gateway with Sharetribe, a few words on the different onboarding and payment experiences the payment gateways offer. The different experiences have implications for the integration, branding, and also the level of PCI-compliance required. There are two main types of onboarding and payment experiences the payment gateways offer: white-label and hosted. However, most providers offer a mix of both. ### White-label onboarding and payments Payment gateways such as [MANGOPAY][mangopay-marketplaces] and [Adyen][adyen-for-platforms] offer a so-called white-label experience. This experience is closest to the default [Stripe Connect][stripe-connect] integration in Sharetribe. A white-label experience means that you build the payment flow inside your marketplace application. This way, you have control over the user-interface and branding. The downside is that the integration requires more coding to build, most likely requires more maintenance, too. The regulations concerning online payments may change, which means you'll need to update your integration accordingly. When using white-label experience, you may also need to do some work regarding PCI-compliance, but more on that later. ### Hosted onboarding and payments Some payment gateways, such as PayPal, offer a hosted experience. In this case, the seller onboarding and customer payments happen by redirecting your user to the payment provider's website. After the customer completes the payment, the payment gateway redirects them to your application to the return URL you provided. This is also the case with seller onboarding: they'll interact with the branded user interface of the payment gateway. With this model, you have limited control over the user-interface on the payment provider's website. However, there is also less coding required from you: The payment provider has already implemented the required forms and hosts them for you. In the case of a well-known payment gateway, their branded payment experience can also increase the perceived security of the payment experience. In case of regulatory changes, the payment provider updates their user-interface to comply with the new regulation. Most likely, you also don't need to do any work regarding PCI-compliance when using a hosted experience. ### Hosted onboarding and white-label payments White-label payment gateways usually offer an option to use hosted pages in some stages of the payment flow. We offer this approach by default with the Stripe integration in Sharetribe, where we use [hosted pages for provider onboarding][stripe-connect-onboarding] and a white-label experience for customer checkout. Using hosted pages for some parts of the payment flow and white-label experience for other parts provides a good balance between the work required from you and the ability to customize the user experience. For example, we've chosen to use Stripe-hosted pages for Sharetribe's provider onboarding because in this step, compliance with the _Know Your Customer_ (KYC) guidelines is critical and may include uploading identity documents or utility bills. Implementing all that in a white-label fashion would require an undesirable amount of work. ## Do you need to be PCI DSS compliant to integrate with a 3rd-party payment gateway? PCI DSS stands for Payment Card Industry Data Security Standards. It is a set of security standards to ensure that companies that accept credit card payments process, store, and transfer the credit card information securely. Since you are operating a marketplace business that accepts payment by credit card, you have to be PCI DSS compliant. However, there are different levels to PCI DSS compliance. You can reduce your required level of compliance significantly by using a payment gateway that offers tools like hosted pages and components or client-side encryption of the credit card information. All of the payment gateways listed in this article ([Adyen][adyen-for-platforms], [PayPal][paypal-commerce-platform], [MANGOPAY][mangopay-marketplaces], and [Stripe][stripe-connect]) offer such tools. ### PCI DSS compliance levels There are four PCI DSS compliance levels, where Level 1 is the strictest. Level 4 is for small-to-medium-sized businesses that process less than 20,000 transactions per year. Most early-stage marketplace entrepreneurs start at this level. The only requirement for becoming Level 4 PCI DSS compliant is to perform a Self-Assessment Questionnaire (SAQ). ### Self-Assessment Questionnaire (SAQ) The PCI Security Standards Council offers several SAQ questionnaire documents. The one you should choose depends on your payment integration. If you outsource credit card information processing to a PCI-compliant 3rd-party payment gateway, the required questionnaire is [Self-Assessment Questionnaire A (SAQ A)][pci-saq-a-pdf]. The SAQ A is relatively short (24 yes/no questions). Some payment gateways prefill the questionnaire for you. [This is, for example, what Stripe does.][stripe-how-helps-pci] ### When and where do I submit the Self-Assessment Questionnaire? Most likely, your payment gateway will contact you and ask you to upload the Self-Assessment Questionnaire and additional documents, if necessary. #### Example: Stripe - If you use [Stripe Checkout/Elements, Mobile SDK, or Connect][stripe-integration-security], Stripe pre-fills the SAQ A for you. - Stripe monitors your transaction volume and notifies if a [growing transaction volume will require a change in how you validate compliance][stripe-how-helps-pci]. #### Example: Adyen - If you use [Pay by Link][adyen-pay-by-link] (hosted experience), Adyen doesn't require you to submit SAQ A. - If you use [Drop-in or Components][adyen-drop-in-or-components] (white-label), Adyen requires you to assess your compliance with SAQ A and submit the filled questionnaire. ### Conclusion about marketplace PCI DSS compliance On some level, you need to be PCI DSS compliant. However, if you are on Level 4, depending on the type of your payment integration, becoming PCI DSS compliant requires little to no work from you. ## Communication between the marketplace app and payment gateway Before integrating a 3rd-party payment gateway into your marketplace, it's good to be aware of the different methods apps can use to communicate with a payment gateway. The most common communication methods are: - API calls - Redirect URLs and Return URLs - Webhooks The communication method to use depends on the onboarding and payment experience, and the payment flow stage. For example, Redirect URLs are only used for hosted onboarding and payment pages. API calls instead are heavily used for white-label onboarding and payments, but they are also needed even when using hosted payments to request the hosted page URL where the user should be redirected. Next, we'll go through all the different communication methods. ### API calls The marketplace can communicate with the payment gateway by calling their API. The marketplace needs to do this for example to request URL for the hosted page or make a payment in a white-label fashion. Even when using hosted experience payments, you still need to make API calls to the payment gateway for some payment actions, such as capturing the payment or refunding a payment. To call the API, the payment gateways require you to use a secret key which you should never expose to the public. Thus, the API calls to the payment gateway need to happen from your marketplace backend, never from the user's browser. ### Redirect URLs and Return URLs When using hosted pages (i.e. your onboarding or payment experience is fully hosted or combines hosted and white-label stages), your app communicates with the payment gateway through data added in the URL. Before redirecting your user to the hosted page, your marketplace makes an API call to the payment gateway to request the URL to the hosted page. At this point, your marketplace provides the payment gateway two different return URLs: one for when a payment is completed by the user, and one for failed or canceled payment. After receiving the hosted page URL from the payment gateway, you redirect the user to that URL. When the user returns from the hosted page, the payment gateway redirects them to one of the return URLs depending on the payment status. ![Redirect and return URL call sequence](./redirect-url-sequence.png) {/* Diagram source: https://whimsical.com/how-to-integrate-a-3rd-party-payment-gateway-PBY6qRjauyb7v5pEdXY4pS */} The payment gateway appends data about the onboarding or payment result to the return URL query parameters. When it redirects the user to your application, you can read this data from the URL and store it for later use. You can use [the Marketplace API's update current user endpoint][marketplace-api-update-user-profile] to store seller's onboarding status to current user's private data or the [Integration API to store payment status to transaction's metadata][integration-api-update-transaction-metadata]. #### Example: PayPal redirect after seller onboarding After successful seller onboarding, PayPal redirects the seller to the return URL and [loads the URL with the following query parameters][paypal-redirect-seller]: - `merchantId` - `merchantIdInPayPal` - `permissionsGranted` - `accountStatus` - `consentStatus` - `productIntentId` - `isEmailConfirmed` - `returnMessage` - `riskStatus` The marketplace should store at least the `merchantId` to current user's private data. Later on, when a customer makes a payment to the seller, the `merchantId` is needed in order to pay for the correct seller. ### Webhooks Payment gateways send webhook notifications that you can listen to. You may want to listen to these notifications in a case where customer onboarding on a hosted page contains multiple steps or when a particular payment action such as payout is happening without user involvement, and you want to know the status of that action. #### Example: PayPal onboarding [PayPal seller onboarding][paypal-track-seller-onboarding] is complete when the following requirements are met: 1. Seller creates a PayPal account. 2. Seller grants you permission for the [features][paypal-rest-endpoint-feature] you set. 3. Seller confirms the email address of the account. The marketplace can track the onboarding status by listening to the webhook notifications. When the marketplace knows the onboarding status, it can be used to provide the seller a useful message in the marketplace user-interface, should as "PayPal account created, but the email address is not yet confirmed. Please confirm the email address in order to complete the onboarding". #### Backend endpoint to listen to webhook notifications To listen to webhooks, you'll need to implement a new endpoint to your backend and configure the payment gateway to send the notifications to that URL. The endpoint should read the notifications and decide what to do with them and where to store the information. If a notification is, for example, about the user's onboarding status, you can use [the Integration API to store the information in the user's private data][integration-api-update-user-profile]. On the other hand, if the notification is about a payment related to a transaction, you may want to store the information in the [transaction's metadata][integration-api-update-transaction-metadata] or [transition the transaction][integration-api-transition-transaction]. If you use a Sharetribe Web Template, you can add a new endpoint by adding it to the [API router][web-template-api-router]. We recommend securing the endpoint with Basic Authentication if the payment gateway supports that. ## Integrating your Sharetribe marketplace with a 3rd-party payment gateway There are two main options for integrating Sharetribe with a 3rd-party payment gateway: - [Privileged transitions][dev-docs-concepts-privileged-transitions] - [Events][dev-docs-refence-events]. For some payment flow stages you can use either method to build the integration, but for some stages using exactly one of the two is required. In the next chapter, we'll give our recommendations on which option to use in each step. First, a quick introduction to how each method works in practice. ### Using privileged transitions To use privileged transitions, you need to make a new endpoint to your backend server. Your marketplace front-end should call this new backend endpoint and not the Sharetribe API directly. If you use Sharetribe Web Template, you can add a new endpoint by adding it to the [API router][web-template-api-router]. The new server endpoint should call the payment gateway API to do the payment action and the Sharetribe Marketplace API to transition the transaction. The following diagram shows the call sequence between the marketplace and the APIs. First, a user makes a transaction request using the marketplace front end, after which the backend calls the Sharetribe Marketplace API to initiate the transaction. Next, the marketplace backend calls the payment gateway's API, and finally the Sharetribe Marketplace API is called again after the payment action is completed. ![Privileged transitions call sequence](./privileged-transitions-sequence.png) {/* Diagram source: https://whimsical.com/how-to-integrate-a-3rd-party-payment-gateway-PBY6qRjauyb7v5pEdXY4pS */} Error handling in this model is simple. Because all the calls to the Sharetribe Marketplace API and the payment gateway's API are triggered by the request of the end-user, your marketplace front-end can immediately return a failure response and show an error message to the user when things go wrong. ### Using Events You can also use Events for your payment gateway integration. In this model, the transaction transition is done as usual. Your backend polls the events and reacts to transaction transition events by calling the payment gateway API. ![Events call sequence](./events-sequence.png) {/* Diagram source: https://whimsical.com/how-to-integrate-a-3rd-party-payment-gateway-PBY6qRjauyb7v5pEdXY4pS */} Error handling in this model needs more attention than in the privileged transition model. If the payment action fails, the transaction is in a state where the transaction and payment are out of sync. **Example:** Let's imagine the customer made a transition "cancel," which moved the transaction to the "canceled" state, but the "refund" payment action failed. If you don't handle this error state, the customer gets charged without receiving the service. There are a couple of ways you can set up Events to handle the error case: - Retry the payment action immediately, in case of a network error or error 5xx from the payment gateway. - Send a notification to the marketplace operator. The operator can then manually resolve the issue (e.g., by manually refunding from the payment gateway's dashboard) - Transition the transaction to an error state, e.g., "canceled-but-refund-failed." The operator then needs to manually resolve the issue with a refund. - Write the error in the transaction's metadata and show it to the user. The user can then contact the operator to resolve the situation. ## Integrating payment steps with your marketplace In the beginning of this document we illustrated a high-level picture of the different payment steps in a marketplace payment flow. In this chapter we'll go through each step and how to integrate them with your marketplace. {

Provider onboarding

} The provider onboarding step usually needs to happen before the customer starts the transaction. Onboarding can occur during listing creation or separately from it. If you choose white-label onboarding, you need to build the necessary forms in your marketplace to ask for the required customer details. Also, to comply with the _Know Your Customer_ (KYC) requirements, the user may need to upload certain documents such as an identity document or a utility bill to prove their identity. If you're using a payment gateway that provides hosted onboarding, you need to redirect the user to the onboarding page hosted by the payment provider. When the user returns from the hosted page, you receive information about the newly created merchant account (e.g., a merchant ID) in the return URL. You should store this information in the user's private data. You can use [the Marketplace API's update current user endpoint for this][marketplace-api-update-user-profile]. **Recommendation:** Use hosted pages for onboarding if they are available. Building the necessary forms, including the file upload forms, can be avoided using hosted pages. Besides, the KYC requirements tend to change over time. By using hosted pages, you avoid the need to change your application when the requirements change. The payment provider will keep the hosted pages up to date with the regulatory requirements. {

Customer checkout

} Customer checkout happens when the customer decides to proceed with the payment, and it's an essential part of your marketplace payment flow. It's also the step that requires the most integration work. Before you start implementation, consider what payment methods you are about to integrate and where your customers are located. Does the payment method have separate authorization and capture actions, or are the funds immediately captured during the payment? Are your customers located in countries that require 3D Security? In this step, your marketplace collects the customer's credit card information. Keep in mind that you should never pass unencrypted credit card information via your server. Use either the JavaScript library/elements provided by the payment gateway, client-side encryption, or hosted pages when collecting credit card information. **Recommendation:** Use **privileged transitions** for this step. **Recommendation:** Initiate the Sharetribe transition before payment. If using 3D Security is required, the checkout process may contain multiple steps where the customer may have to leave your marketplace app and go to their bank's website to confirm the payment. This process may take some time for the user to complete. Because of that, we recommend that you initiate the Sharetribe transition first if it does not already exist, and only after that you initiate the payment. During the initialization of the transaction, you can already make a booking and ensure availability. It would be a bad experience for the user to pay first but then realize that the transaction initialization failed because someone else managed to book the selected timeslot a moment before they did. The transaction should be in a "pending-payment" state at this point. After the payment is successfully made, transition the transaction forward to a "paid" state. The "pending-payment" state should have an automatic expiration, e.g., after 15 minutes. The user might not finalize the payment, and thus we need to expire the payment in the transaction. If the payment is made when the transaction is first initiated, the transaction process should look something like this: ![Customer checkout transaction process](./checkout-transaction-process.png) {/* Diagram source: https://whimsical.com/how-to-integrate-a-3rd-party-payment-gateway-PBY6qRjauyb7v5pEdXY4pS */} The steps to implement this stage are: 1. Add ([or modify existing][template-initiate-privileged]) server endpoint for initiating a transaction. 2. Initiate the transaction. 3. Make the payment after the transaction is initiated. 4. Ensure that the payment went through. 5. Transition the transaction forward. If the transaction already exists, for example because of an earlier inquiry transition or a negotiation process, steps 1-2 would involve transitioning the transaction instead. {

Provider accept

} Depending on the payment method, the payment may happen in one or two steps. The two steps are: - Authorization (payment gateway reserves the money from the buyer's credit card) - Capture (payment gateway transfers the money from the buyer) Credit card payments use these two steps, whereas some payment methods like [giropay][giropay] or [iDEAL][ideal] have only one step where the payment is authorized and captured in one go. If you're using a payment method where authorization and capturing happens in one step, you should skip the provider accept stage, and implement a so-called "instant booking" flow. In this flow, the provider is expected to be able to provide the service or product to the customer, thus, no separate accept stage is needed. For this to happen, the providers must keep their availability calendar and item stock up-to-date. Even if you're using payment method with separate authorization and capture steps, it's worth considering whether you could streamline the transaction process by implementing an instant book flow. In case you decide to have the provider accept state to your transaction process, you should do the payment authorization on customer checkout and capture the payment on provider accept. After the money is captured, you most likely need to pay the payment gateway's processing fees, even if the provider ends up declining the customer's request. If the provider declines the request, or if it expires due to a lack of answer from the provider, the payment should be voided. **Recommendation:** Use **events** for this step. Poll the transaction/transition events and react on accept/decline/expire transitions. Call the payment gateway API to either capture or void the payment. {

Customer refund

} In most marketplaces, it makes sense to have a delayed payment period. During this period, the customer has paid, but the money is not yet paid out to the provider. The provider is expected to provide the service first, and payout happens after the service was successfully provided. Customer refund usually happens during this period. **Recommendation:** Use **events** for this step. Poll the transaction/transition events and react on transitions where a refund is needed by calling the payment gateway API. {

Provider payout

} At some point, it's time to release the money to the provider and do the payout. Depending on the provider's bank, there's usually a delay between when the payout is issued and when the money reaches the provider's bank account. The delay is on a scale of days, but not weeks. **Recommendation:** Use **events** for this step. Poll the transaction/transition events and react on transitions where the payout is needed by calling the payment gateway API. ## Summary In this document, we first illustrated on a high-level a typical marketplace payment flow. After that, we went through the different payment gateway integration experiences, white-label and hosted, and their implications to the integration, branding, and PCI DSS compliance requirements. We then discussed PCI DSS compliance in more detail. We discussed the integration and what are the options to communicate with payment gateways. After that, we laid out the two different options to hook your payment integration code with Sharetribe: privileged transitions and events. Finally, we went through each stage of the marketplace payment flow and gave recommendations on how to integrate a 3rd-party payment gateway to each step. ## Further reading - [Get Started with Platforms start by Adyen][adyen-platforms-get-started] - [PayPal Commerce Platform for Marketplaces and Platforms developer documentation][paypal-commerce-platform-docs] - [MANGOPAY API Documentation][mangopay-api-docs] [academy-payment-service-providers]: https://www.sharetribe.com/academy/marketplace-payments/ [paypal-commerce-platform]: https://www.paypal.com/us/business/platforms-and-marketplaces [paypal-commerce-platform-docs]: https://developer.paypal.com/docs/platforms/ [paypal-redirect-seller]: https://developer.paypal.com/docs/platforms/seller-onboarding/before-payment/#4-redirect-seller [paypal-rest-endpoint-feature]: https://developer.paypal.com/docs/api/partner-referrals/v2/#definition-rest_endpoint_feature [paypal-track-seller-onboarding]: https://developer.paypal.com/docs/platforms/seller-onboarding/before-payment/#5-track-seller-onboarding-status [mangopay-marketplaces]: https://www.mangopay.com/marketplaces/ [mangopay-api-docs]: https://docs.mangopay.com/ [adyen-for-platforms]: https://www.adyen.com/platform-payments [adyen-platforms-get-started]: https://docs.adyen.com/platforms/get-started/ [adyen-pay-by-link]: https://docs.adyen.com/development-resources/pci-dss-compliance-guide?tab=pay_by_link_1#online-payments [adyen-drop-in-or-components]: https://docs.adyen.com/development-resources/pci-dss-compliance-guide?tab=drop_in_or_components_2#online-payments [stripe-connect]: https://stripe.com/connect [stripe-connect-onboarding]: https://stripe.com/connect/onboarding [stripe-how-helps-pci]: https://stripe.com/en-fi/guides/pci-compliance#how-stripe-helps-organizations-achieve-and-maintain-pci-compliance [stripe-integration-security]: https://stripe.com/docs/security/guide#validating-pci-compliance [giropay]: https://www.giropay.de/ [ideal]: https://www.ideal.nl/en/ [dev-docs-concepts-privileged-transitions]: /concepts/transactions/privileged-transitions/ [dev-docs-refence-events]: /references/events/ [dev-docs-howto-reacting-to-events]: /how-to/events/reacting-to-events/ [dev-docs-reference-extended-data]: /references/extended-data/ [pci-saq-a-pdf]: https://www.pcisecuritystandards.org/documents/PCI-DSS-v3_2_1-SAQ-A.pdf [marketplace-api-update-user-profile]: https://www.sharetribe.com/api-reference/marketplace.html#update-user-profile [integration-api-update-transaction-metadata]: https://www.sharetribe.com/api-reference/integration.html#update-transaction-metadata [integration-api-update-user-profile]: https://www.sharetribe.com/api-reference/integration.html#update-user-profile [integration-api-transition-transaction]: https://www.sharetribe.com/api-reference/integration.html#transition-transaction [web-template-api-router]: https://github.com/sharetribe/web-template/blob/main/server/apiRouter.js [template-initiate-privileged]: https://github.com/sharetribe/web-template/blob/main/server/api/initiate-privileged.js --- ## How to integrate Apple Pay and Google Pay Path: how-to/payments/payment-request-button/index.mdx # How to integrate Payment Request Button for Apple Pay and Google Pay The Sharetribe Stripe integration supports using card wallets such as Google Pay and Apple Pay without transaction process changes, when using the Payment Request Button. This guide shows you how to integrate the Payment Request Button on your Sharetribe Web Template checkout page. ## Why Payment Request Button Stripe's own documentation recommends using [Express Checkout Element](https://docs.stripe.com/elements/express-checkout-element) or [Payment Element](https://docs.stripe.com/payments/payment-element) in new integrations. However, neither of those elements is easily compatible with the Sharetribe Stripe default integration: - The Express Checkout Element does not directly support [manual capture](https://docs.stripe.com/payments/place-a-hold-on-a-payment-method), and the Sharetribe backend creates payment intents with `capture_method: manual` to support preauthorizing the funds while a transaction is waiting for the provider to accept or decline. - The Payment Element collects payment details through Stripe Elements using automatic payment methods i.e. with a Payment Element internal logic. The Sharetribe backend creates the payment intent with a `payment_method_types` parameter that defines what payment method types are allowed to complete the payment intent. The confirm function stripe.confirmPayment expects the Payment Element to set the payment methods instead of the payment intent, so it is incompatible with Sharetribe-created payment intents. The [Payment Request Button](https://docs.stripe.com/stripe-js/elements/payment-request-button) is a Stripe Element for displaying wallet payment methods that avoids these pitfalls. It works with manual capture, so it avoids the issue with Express Checkout Element. In addition, it uses the same `stripe.confirmCardPayment` function as the default card payment flow, so it does not have the same confirm issue as the Payment Element. In this guide, we'll add Payment Request Button to CheckoutPageWithPayment. You will notice that a lot of the structures for Payment Request Button parallel the existing structures for card payments, so being familiar with the existing card payment flow is useful but not required. ### Prerequisites Before you start developing this feature, make sure you have taken the following steps to prepare. #### Serve your application over HTTPS. The Payment Request Button does not mount over plain HTTP. For local development, use a tunnel such as [ngrok](https://ngrok.com/). When you're using the `yarn run dev-server` command to run the template, this command overwrites the `REACT_APP_MARKETPLACE_ROOT_URL` specified in the .env file. Ensure you update the command to match the URL where your site is being served through Ngrok. #### Register your domain Register your domain with Stripe in both sandbox and live mode [according to these instructions](https://docs.stripe.com/payments/payment-methods/pmd-registration). The button will not appear on unregistered domains. #### Add a card to a wallet on the device and browser you will test with With Chrome, the Google Pay interface will automatically show up when you are using test keys. Make sure that your Chrome settings aren't blocking cookies and that you're using Standard (not enhanced) protection. Safari will not allow testing with Stripe Test keys, since it is not possible to add a Stripe test card in your Apple Wallet. This means that you will need to develop the feature with another browser and then test with Safari once you are using Live keys. #### Add a server endpoint to fetch the provider's Stripe Connect account The payment request initialisation requires the Stripe Connect account id that matches the on_behalf_of account in the Payment Intent. The Marketplace API does not expose the attribute at the moment, so you will need to add a server-side endpoint that - verifies the request is coming from an authenticated marketplace user - calls the Integration API users.show with the provider's id - and then returns the provider's Stripe Connect account id to the client. You'd then need to pass on the Stripe Connect account as a prop to the StripePaymentForm component, where the payment request is initialised. This implementation expects the attribute as `providerStripeAccountId`. ## Add Payment Request Button ### Add Stripe country to config To create `stripe.paymentRequest`, we need the country code of the platform operator's Stripe account. First, add your country code in your .env file as `REACT_APP_STRIPE_COUNTRY`: ```shell filename=".env" REACT_APP_STRIPE_COUNTRY=DE; ``` Add that as an export in `src/config/configStripe.js`. Include a fallback value so a missing env variable does not silently break the Payment Request Button initialization. ```jsx filename="configStripe.js" export const country = process.env.REACT_APP_STRIPE_COUNTRY || 'US'; ``` ### Extend `confirmCardPayment` in stripe.duck.js The `confirmCardPaymentPayloadCreator` function builds `args` as either `[secret]` or `[secret, paymentParams]` in the default implementation. Payment Request Button uses two-phase confirmation. This means that a third branch for `[secret, paymentParams, confirmOptions]` is needed for the Payment Request Button flow, since it requires passing `{ handleActions: false }` as a third argument to the `stripe.confirmCardPayment` thunk on the first call. Add `confirmOptions` to the destructuring of `params`, then replace the two-branch ternary with a three-branch version: ```diff filename="src/ducks/stripe.duck.js" const confirmCardPaymentPayloadCreator = async (params) => { - const { stripe, paymentParams, stripePaymentIntentClientSecret: secret } = params; + const { stripe, paymentParams, confirmOptions, stripePaymentIntentClientSecret: secret } = params; - const args = paymentParams ? [secret, paymentParams] : [secret]; + const args = + paymentParams && confirmOptions + ? [secret, paymentParams, confirmOptions] + : paymentParams + ? [secret, paymentParams] + : [secret]; return stripe.confirmCardPayment(...args); }; ``` ### Add `processCheckoutWithPRBPayment` to CheckoutPageTransactionHelpers.js The default card payment flow uses a function called `processCheckoutWithPayment` to handle the card payment steps. We will add a parallel function, called `processCheckoutWithPRBPayment`, that uses a similar flow but for Payment Request Button specific steps. The function composes the first three async steps from `processCheckoutWithPayment`, but replaces the standard card confirmation with a two-phase Payment Request Button confirmation: - `fnRequestPayment` is identical to the card flow. It calls `onInitiateOrder`, handles the inquiry/negotiation/standard transition branches, and persists the transaction. - `fnConfirmCardPaymentPRB` is the two-phase Payment Request Button confirmation mentioned in the previous step. 1. The first call uses `{ handleActions: false }` and `payment_method: ev.paymentMethod.id`. If it fails, it calls `ev.complete('fail')` before throwing. If it succeeds, the flow calls `ev.complete('success')` to close the payment sheet, and then checks whether the second call is necessary. 2. A second call is made if `paymentIntent.status === 'requires_action'` – for example to handle 3DS or other actions required by the payment intent. - `fnConfirmPayment` — identical to the card flow. It transitions the transaction using the Marketplace API. The `processCheckoutWithPayment` function has a fourth step for saving a default payment method. However, wallet payment methods cannot be saved as default, so we don't include that step. Instead, we add a `.then` block to the composed function to return a correctly shaped response. Add this new helper function to `CheckoutPageTransactionHelpers.js`: ```jsx filename="src/containers/CheckoutPage/CheckoutPageTransactionHelpers.js" export const processCheckoutWithPRBPayment = ( orderParams, extraPaymentParams ) => { const { ev, // Stripe paymentmethod event object hasPaymentIntentUserActionsDone, onConfirmCardPayment, onConfirmPayment, onInitiateOrder, pageData, paymentIntent, process, setPageData, sessionStorageKey, stripe, } = extraPaymentParams; const storedTx = ensureTransaction(pageData.transaction); const processAlias = pageData?.listing?.attributes?.publicData?.transactionProcessAlias; let createdPaymentIntent = null; // Step 1: fnRequestPayment – identical to processCheckoutWithPayment const fnRequestPayment = (fnParams) => { const hasPaymentIntents = storedTx.attributes.protectedData?.stripePaymentIntents; const isOfferPendingInNegotiationProcess = resolveLatestProcessName(processAlias.split('/')[0]) === NEGOTIATION_PROCESS_NAME && storedTx.attributes.state === `state/${process.states.OFFER_PENDING}`; const requestTransition = storedTx?.attributes?.lastTransition === process.transitions.INQUIRE ? process.transitions.REQUEST_PAYMENT_AFTER_INQUIRY : isOfferPendingInNegotiationProcess ? process.transitions.REQUEST_PAYMENT_TO_ACCEPT_OFFER : process.transitions.REQUEST_PAYMENT; const isPrivileged = process.isPrivileged(requestTransition); const orderPromise = hasPaymentIntents ? Promise.resolve(storedTx) : onInitiateOrder( fnParams, processAlias, storedTx.id, requestTransition, isPrivileged ); orderPromise.then((order) => { persistTransaction( order, pageData, storeData, setPageData, sessionStorageKey ); }); return orderPromise; }; // Step 2: PRB two-phase confirmation, replaces fnConfirmCardPayment const fnConfirmCardPaymentPRB = async (fnParams) => { const order = fnParams; const hasPaymentIntents = order?.attributes?.protectedData?.stripePaymentIntents; if (!hasPaymentIntents) { throw new Error( `Missing StripePaymentIntents key in transaction's protectedData. Check that your transaction process is configured to use payment intents.` ); } const { stripePaymentIntentClientSecret } = order.attributes.protectedData.stripePaymentIntents.default; // If PI user actions are already done (e.g. a previous attempt succeeded), // close the sheet and pass through if (hasPaymentIntentUserActionsDone) { ev.complete('success'); return { transactionId: order?.id, paymentIntent }; } // First call: confirm PI attaching the PRB payment method, without handling // any required actions. This returns quickly so ev.complete() can be called promptly. let firstResult; try { firstResult = await onConfirmCardPayment({ stripe, stripePaymentIntentClientSecret, paymentParams: { payment_method: ev.paymentMethod.id }, confirmOptions: { handleActions: false }, // extended in Step 2 above orderId: order?.id, }); } catch (err) { // Confirmation failed — tell the browser to re-show the payment interface ev.complete('fail'); throw err; } // Close the browser payment sheet before handling any further actions. // Must be called promptly; Stripe will timeout if delayed. ev.complete('success'); createdPaymentIntent = firstResult.paymentIntent; // Second call: handle 3DS or other required actions if the PI needs them. if (firstResult.paymentIntent?.status === 'requires_action') { const secondResult = await onConfirmCardPayment({ stripe, stripePaymentIntentClientSecret, orderId: order?.id, // No paymentParams, no confirmOptions — let Stripe handle the action }); createdPaymentIntent = secondResult.paymentIntent; return { transactionId: order?.id, paymentIntent: secondResult.paymentIntent, }; } return { transactionId: order?.id, paymentIntent: firstResult.paymentIntent, }; }; // Step 3: complete order – identical to processCheckoutWithPayment const fnConfirmPayment = (fnParams) => { createdPaymentIntent = fnParams.paymentIntent; const transactionId = fnParams.transactionId; const transitionName = process.transitions.CONFIRM_PAYMENT; const isTransitionedAlready = storedTx?.attributes?.lastTransition === transitionName; const orderPromise = isTransitionedAlready ? Promise.resolve(storedTx) : onConfirmPayment(transactionId, transitionName, {}); orderPromise.then((order) => { persistTransaction( order, pageData, storeData, setPageData, sessionStorageKey ); }); return orderPromise; }; const applyAsync = (acc, val) => acc.then(val); const composeAsync = (...funcs) => (x) => funcs.reduce(applyAsync, Promise.resolve(x)); const handlePRBPaymentIntentCreation = composeAsync( fnRequestPayment, fnConfirmCardPaymentPRB, fnConfirmPayment ); return handlePRBPaymentIntentCreation(orderParams).then((order) => ({ orderId: order?.id, paymentMethodSaved: true, })); }; ``` ### Update StripePaymentForm.js The Payment Request Button logic lives inside `StripePaymentForm`. This is because when the customer authorizes the payment, the `paymentmethod` event handler needs to read the form values present at that moment – this way, the transaction fields get included in the submit event. The form values are available via `this.finalFormAPI`. #### Add `prCanMakePayment` to `initialState` Next, let's add `prCanMakePayment: false` to `initialState`. This state attribute drives whether the Payment Request Button section is rendered. Without it, the divider renders for all users regardless of wallet support. In a later step, we'll set it to `true` in `initializePRButton` inside `canMakePayment().then(result => { if (result) { ... } })`. ```jsx filename="src/containers/CheckoutPage/StripePaymentForm/StripePaymentForm.js" const initialState = { // existing state... prCanMakePayment: false, }; ``` #### Add Payment Request Button instance variables and bindings to the constructor Next, add these three bindings and three instance variables alongside the existing `this.cardContainer`: ```jsx filename="src/containers/CheckoutPage/StripePaymentForm/StripePaymentForm.js" // Instance variables this.prContainer = null; // DOM mount target this.pr = null; // stripe.paymentRequest() instance this.prButton = null; // mounted paymentRequestButton Element // Bindings this.initializePRButton = this.initializePRButton.bind(this); this.handlePRButtonRef = this.handlePRButtonRef.bind(this); this.handlePRBPaymentMethod = this.handlePRBPaymentMethod.bind(this); ``` #### Add `initializePRButton` method The default card payment element is initialized with `initializeStripeElement` in StripePaymentForm.js. For Payment Request Button, we will add a parallel function called `initializePRButton`. This function will do 4 things: 1. check that the prerequisites are in place 2. initialize a new `stripe.paymentRequest` 3. if the user has valid payment methods, mount the Payment Request Button with the initialized paymentRequest 4. set the payment request callback for selecting a payment method ```jsx filename="src/containers/CheckoutPage/StripePaymentForm/StripePaymentForm.js" initializePRButton(element) { const { payinTotal, stripeCountry, providerStripeAccountId } = this.props; // if given, "element" is the DOM node from "ref" const container = element || this.prContainer; if (!payinTotal || !this.stripe || this.pr || providerStripeAccountId) { return }; // Use the transaction's payinTotal to set currency and amount // for the payment request. const pr = this.stripe.paymentRequest({ country: stripeCountry, currency: payinTotal.currency.toLowerCase(), total: { label: 'Total', amount: payinTotal.amount, }, requestPayerEmail: true, onBehalfOf: providerStripeAccountId, }); // pr.canMakePayment() checks to make sure that the user has an active payment method – // Payment Request Button can only be mounted if that's the case pr.canMakePayment().then(result => { if (result) { const elements = this.stripe.elements(); const prButton = elements.create('paymentRequestButton', { paymentRequest: pr }); prButton.mount(container); this.prButton = prButton; this.setState({ prCanMakePayment: true }); } }); // Set this.handlePRBPaymentMethod as the 'paymentmethod' // callback of the payment request pr.on('paymentmethod', this.handlePRBPaymentMethod); this.pr = pr; } ``` #### Update `handleStripeJsLoadedEvent` Add the following block at the end of `handleStripeJsLoadedEvent`, after the `initializeStripeElement()` call: ```jsx filename="src/containers/CheckoutPage/StripePaymentForm/StripePaymentForm.js" const { payinTotal } = this.props; if (payinTotal && !this.pr) { this.initializePRButton(); } ``` Because `initializePRButton` assigns `this.pr = this.stripe.paymentRequest(...)` before the async `canMakePayment()` call, `this.pr` is set synchronously. By the time `canMakePayment` resolves, the component has rendered and `this.prContainer` is populated via `handlePRButtonRef`, so the button mounts correctly. #### Add `handlePRButtonRef` and `handlePRBPaymentMethod` The existing `handleStripeElementRef` callback ref stores the incoming DOM node in `this.cardContainer`, and then calls `initializeStripeElement(el)` if Stripe is already initialized. We will add a new callback ref `handlePRButtonRef` that follows the same pattern — it stores the DOM node in `this.prContainer`, and then calls `initializePRButton(el)` if Stripe is ready. In the card flow, FinalForm calls `handleSubmit` directly and the current field values are passed in automatically. In the Payment Request Button flow, the browser fires a `paymentmethod` event instead of triggering a form submit, so the form values are not passed automatically. The `handlePRBPaymentMethod` function reads them explicitly from `this.finalFormAPI.getState().values`. ```jsx filename="src/containers/CheckoutPage/StripePaymentForm/StripePaymentForm.js" handlePRButtonRef(el) { this.prContainer = el; if (this.stripe && el) { this.initializePRButton(el); } } handlePRBPaymentMethod(ev) { const formValues = this.finalFormAPI ? this.finalFormAPI.getState().values : {}; if (this.props.onPRBPaymentMethod) { this.props.onPRBPaymentMethod(ev, this.stripe, formValues); } else { ev.complete('fail'); } } ``` #### Add `componentDidUpdate` for deferred Payment Request Button initialization The card element is initialized in `handleStripeJsLoadedEvent` because the Stripe publishable key is always available by the time it runs. `payinTotal` is different: it comes from the speculative transaction call, which is often still in flight when Stripe initializes. When `payinTotal` is `null` at that point, `initializePRButton` returns early and the button is never shown. The way `componentDidUpdate` solves this is by watching for the moment `payinTotal` first becomes available. The `!this.pr` check prevents a duplicate initialize in case `handleStripeJsLoadedEvent` already succeeded. Updating `payinTotal` on the checkout page is not something the Sharetribe Web Template supports by default. If this is something that you have added with customization, you will need to include logic to also handle updating `payinTotal`. If `payinTotal` changes after initialization — for example, when a quantity is updated — `this.pr.update()` keeps the amount on the payment sheet in sync without recreating the button. ```jsx filename="src/containers/CheckoutPage/StripePaymentForm/StripePaymentForm.js" componentDidUpdate(prevProps) { const { payinTotal } = this.props; if (!prevProps.payinTotal && payinTotal && this.prContainer && !this.pr) { this.initializePRButton(); } if (prevProps.payinTotal?.amount !== payinTotal?.amount && this.pr) { this.pr.update({ total: { label: 'Total', amount: payinTotal.amount } }); } } ``` #### Add Payment Request Button cleanup to `componentWillUnmount` The existing `componentWillUnmount` removes the card's change event listener and calls `this.card.unmount()` to detach the card Element. Payment Request Button has two separate objects to clean up, and their cleanup APIs are not the same. `this.prButton` is a Stripe Element, so it uses `.unmount()` — the same method used for the card. `this.pr` is a `paymentRequest` instance rather than a Stripe Element, so it does not have `.unmount()`. Instead, unregister the `paymentmethod` listener with `.off()`, which is the mirror of the `.on()` call in `initializePRButton`. ```diff filename="src/containers/CheckoutPage/StripePaymentForm/StripePaymentForm.js" componentWillUnmount() { window.removeEventListener(STRIPE_JS_LOADED_EVENT, this.handleStripeJsLoadedEvent); if (this.card) { this.card.removeEventListener('change', this.handleCardValueChange); this.card.unmount(); this.card = null; } + + if (this.prButton) { + this.prButton.unmount(); + this.prButton = null; + } + if (this.pr) { + this.pr.off('paymentmethod', this.handlePRBPaymentMethod); + this.pr = null; + } } ``` #### Render the Payment Request Button button in `paymentForm()` The card element div already uses the ref-callback pattern — `ref={this.handleStripeElementRef}` — to trigger initialization at the moment the DOM node is available. The Payment Request Button div uses the same pattern with `ref={this.handlePRButtonRef}`. `!askShippingDetails` hides the entire section for listings that require shipping, since the Payment Request Button does not collect a shipping address. If you want to use Payment Request Button with listings that require shipping, you'll need to configure for example a transaction field to collect that information. Do not gate `showPaymentRequestButton` on `prCanMakePayment`. If the container div were gated on `prCanMakePayment`, the `handlePRButtonRef` callback could never fire, `this.prContainer` would never be set, and `initializePRButton` would never run — creating a circular dependency that prevents the button from ever appearing. Instead, gate only the divider on `prCanMakePayment`, so the "or pay with card" text does not appear until Stripe has confirmed a wallet is available. Place the PRB section as the **first child of ``**, before ``, following Stripe's recommendation to place the wallet button above the card form. If your marketplace uses transaction fields, you'll need to rework the layout so that transaction fields appear above the payment method elements. ```diff filename="src/containers/CheckoutPage/StripePaymentForm/StripePaymentForm.js" const { prCanMakePayment } = this.state; const showPaymentRequestButton = !askShippingDetails; return hasStripeKey ? ( + {showPaymentRequestButton ? ( +
+
+ {prCanMakePayment ? ( +
+ + + +
+ ) : null} +
+ ) : null}+ {/* ...rest of form */} ) : ... ``` Add the CSS for the new elements to `StripePaymentForm.module.css`: ```css filename="src/containers/CheckoutPage/StripePaymentForm/StripePaymentForm.module.css" .paymentRequestButtonSection { margin-bottom: 24px; } .paymentRequestButton { /* Stripe fills this element to 100% width */ } .paymentRequestDivider { display: flex; align-items: center; margin: 16px 0; gap: 12px; } .paymentRequestDivider::before, .paymentRequestDivider::after { content: ''; flex: 1; height: 1px; background: var(--colorGrey100); } .paymentRequestDividerText { font-size: 14px; color: var(--colorGrey500); white-space: nowrap; } ``` And add the translation key to `src/translations/en.json`: ```json filename="src/translations/en.json" "StripePaymentForm.orPayWithCard": "or pay with card", ``` #### Destructure new props in `render()` The `render()` function already destructures `onSubmit` before spreading `...rest` into ``, because `onSubmit` is consumed by the class instance and must not reach FinalForm as an unrecognized prop. We have introduced three new props that should follow the same rule: `payinTotal` and `stripeCountry` are consumed by `initializePRButton`, and `onPRBPaymentMethod` is consumed by `handlePRBPaymentMethod`. Destructure all three alongside `onSubmit` so they are excluded from `...rest`. ```diff filename="src/containers/CheckoutPage/StripePaymentForm/StripePaymentForm.js" render() { const { onSubmit, + payinTotal, + stripeCountry, + onPRBPaymentMethod, ...rest } = this.props; // ... } ``` #### Update Content Security Policy in server/csp.js The existing `server/csp.js` already has `connectSrc` and `frameSrc` directives for Stripe, which the default card payment flow requires. The Payment Request Button adds new usages that need additional entries in both directives. When the user taps the Payment Request Button, Stripe opens its payment sheet in an iframe. This iframe loads assets from multiple domains that must be present in `frameSrc` or the iframe is blocked. The button also makes XHR calls to Stripe and Google Pay during payment sheet initialization and confirmation, so the related domains need to be added to `connectSrc`. In `csp.js`, extend the existing directives by concatenating the new domains onto the current `defaultDirectives` values. ```diff filename="server/csp.js" + const { connectSrc = [self] } = defaultDirectives; + const extendedConnectSrc = connectSrc.concat( + '*.google.com', + '*.stripe.com', + '*.stripe.network', + 'api.stripe.com' + ); + const { frameSrc = [self] } = defaultDirectives; + const extendedFrameSrc = frameSrc.concat( + '*.google.com', + '*.stripe.com', + '*.stripe.network', + 'js.stripe.com', + 'hooks.stripe.com' + ); const customDirectives = { + connectSrc: extendedConnectSrc, + frameSrc: extendedFrameSrc, }; ``` #### Wire `handlePaymentRequest` in CheckoutPageWithPayment.js ##### Import `processCheckoutWithPRBPayment` Add `processCheckoutWithPRBPayment` to the named import from `CheckoutPageTransactionHelpers.js`: ```jsx filename="src/containers/CheckoutPage/CheckoutPageWithPayment.js" // existing imports... processCheckoutWithPRBPayment, } from './CheckoutPageTransactionHelpers'; ``` ##### Add `handlePaymentRequest` module-level function The default module-level function `handleSubmit` in `CheckoutPageWithPayment.js` - extracts form values - builds `orderParams` - calls `processCheckoutWithPayment` - and then navigates to `OrderDetailsPage`. We'll add `handlePaymentRequest` as its counterpart for the Payment Request Button flow, and most of its body is identical — the differences follow from two things the PRB flow does differently. First, the Stripe payment sheet has its own in-progress state. When `submitting` is already true, `handlePaymentRequest` calls `ev.complete('fail')` before returning, which closes the payment sheet immediately. `handleSubmit` can simply return early because the card form has no sheet to close. Second, in the card `handleSubmit`, the `stripe` instance is passed in directly from the `onStripeInitialized` callback stored in component state. In `handlePaymentRequest`, the Stripe instance comes from `StripePaymentForm.handlePRBPaymentMethod` as `stripeInstance`, so it is passed in as a function argument rather than read from state. In `handlePaymentRequest`, `orderParams` is built with `getOrderParams`, passing `shippingDetails` as an empty object because the Payment Request Button does not collect a shipping address. `onSavePaymentMethod` is not necessary because wallet payment methods cannot be saved as a default Sharetribe payment method. Like `handleSubmit`, `handlePaymentRequest` must call `onSubmitCallback()` after a successful payment and `setOrderPageInitialValues` before navigating to `OrderDetailsPage`. ```jsx filename="src/containers/CheckoutPage/CheckoutPageWithPayment.js" const handlePaymentRequest = ( ev, stripeInstance, formValues, process, props, submitting, setSubmitting ) => { if (submitting) { ev.complete('fail'); return; } setSubmitting(true); const { history, config, routeConfiguration, speculatedTransaction, paymentIntent, dispatch, onInitiateOrder, onConfirmCardPayment, onConfirmPayment, onSubmitCallback, pageData, setPageData, sessionStorageKey, transactionFieldConfigs = [], } = props; const { message } = formValues; const transactionFieldsProtectedData = { ...pickTransactionFieldsData( formValues, 'protected', true, transactionFieldConfigs ), }; const shippingDetails = {}; const optionalPaymentParams = {}; const customerDefaultMessage = message ? message.trim() : null; const orderParams = getOrderParams( pageData, shippingDetails, optionalPaymentParams, config, transactionFieldsProtectedData, customerDefaultMessage ); const hasPaymentIntentUserActionsDone = paymentIntent && STRIPE_PI_USER_ACTIONS_DONE_STATUSES.includes(paymentIntent.status); const prbPaymentParams = { ev, pageData, speculatedTransaction, stripe: stripeInstance, paymentIntent, hasPaymentIntentUserActionsDone, process, onInitiateOrder, onConfirmCardPayment, onConfirmPayment, sessionStorageKey, setPageData, }; processCheckoutWithPRBPayment(orderParams, prbPaymentParams) .then((response) => { const { orderId, paymentMethodSaved } = response; setSubmitting(false); const orderDetailsPath = pathByRouteName( 'OrderDetailsPage', routeConfiguration, { id: orderId.uuid, } ); const initialValues = { savePaymentMethodFailed: !paymentMethodSaved, }; setOrderPageInitialValues( initialValues, routeConfiguration, dispatch ); onSubmitCallback(); history.push(orderDetailsPath); }) .catch((err) => { console.error(err); setSubmitting(false); }); }; ``` ##### Pass new props to `` The three new props to `` follow the same pattern as `totalPrice` and `stripePublishableKey` from `config`. `payinTotal` is passed from `speculatedTransaction?.attributes.payinTotal` rather than from `existingTransaction`, as `existingTransaction` only exists if the transaction was initiated with an inquiry whereas `speculatedTransaction` is fetched on page load. `stripeCountry` comes from `config.stripe.country`, the value we added earlier in this guide. ```jsx filename="src/containers/CheckoutPage/CheckoutPageWithPayment.js" handlePaymentRequest( ev, stripeInstance, formValues, process, props, submitting, setSubmitting ) } /> ``` And that's it! With these steps, you have added Payment Request Button to your checkout page and enabled your customers to use Apple Pay or Google Pay. ![Payment Request Button showing Google Pay](./google-pay-prb.png) --- ## How to remove Stripe and payments Path: how-to/payments/removing-stripe-and-payments/index.mdx # How to remove Stripe and payments In this guide, we will go through the minimum changes needed to make the booking flow work without Stripe. This is not an easy customization project. ## Removing Stripe The Sharetribe Web Template works with specific transaction processes and payments are a core feature of the booking, purchase, and negotiation processes: the ubiquitous nature means that you need to touch several components and files. When you remove Stripe payments, you may need to customize some parts (e.g. transaction process, email templates, checkout page) more depending on your marketplace idea. For example, you might also want to remove unused Stripe components from your project to clean up the code. The template supports the _default-inquiry_ process, which does not include bookings or stock reservations. This process is the easiest way to use a marketplace without Stripe. However, if you do want to use bookings, stock reservations, or negotiation without Stripe, you will need to follow the steps in this guide. To follow these instructions, the transaction process you are modifying needs to **already exist in your marketplace**. If you want to add a new transaction process without Stripe actions (for example `default-booking-no-stripe/release-1`) instead of making a new version of your existing one (for example `default-booking/release-1`), you first need to add an unmodified version of it to your marketplace by following these instructions: - [Create a new transaction process with the Sharetribe CLI](/how-to/transaction-process/create-new-transaction-process-with-cli/) - [Change transaction process in Sharetribe Web Template](/how-to/transaction-process/change-transaction-process-in-template/) ### Update the transaction processes and remove Stripe related actions You need to remove at least the following actions: - `:action/stripe-create-payment-intent` - `:action/stripe-capture-payment-intent` - `:action/stripe-confirm-payment-intent` - `:action/stripe-create-payout` - `:action/stripe-refund-charge` - `:action/stripe-refund-payment` If you are using the template default processes with strong customer authentication (e.g. `default-booking`, `default-purchase`, `default-negotiation`) you need to remove confirming the payment. Otherwise, the transaction will get stuck. The simplest way to do this is to remove `pending-payment` and `expire-payment` states and point `request-payment` and `request-payment-after-inquiry`, or `request-payment-to-accept-offer`, directly to the following state: - `preauthorized` in `default-booking` - `purchased` in `default-purchase` - `offer-accepted` in `default-negotiation` When you are building the transaction process for your marketplace, it may make sense to rename some of the states and transitions so that they make more sense in your use-case. For instance, _preauthorized_ state refers to the state of payment - it is preauthorized to be captured. Without payments in the process, it can be a good idea to change the state name to something else. This guide proceeds with the assumption that you have not changed state and transition names in your process. If you do change your transition and state names, you will need to double check all example code and files to make sure you have updated the transition and state names referenced in those files. ### Edit the transaction process file to reflect the changes in your transaction process The [src/transactions/transaction.js](https://github.com/sharetribe/web-template/blob/main/src/transactions/transaction.js) has a number of helper functions that are used to determine which state the transaction is. Depending on the transaction process being used, the _transaction.js_ file refers to either _src/transactions/transactionProcessBooking.js_, _src/transactions/transactionProcessPurchase.js_ or _src/transactions/transactionProcessNegotiation.js_ for the correct states in each process. The transaction process file should be updated to match the new transaction process. [Read more about editing transaction related files](/how-to/transaction-process/change-transaction-process-in-template/#update-the-relevant-files-in-srctransactions-folder). Example: ```diff filename="transactionProcessBooking.js" [STATE_INITIAL]: { on: { [TRANSITION_INQUIRE]: STATE_INQUIRY, - [TRANSITION_REQUEST_PAYMENT]: STATE_PENDING_PAYMENT, + [TRANSITION_REQUEST_PAYMENT]: STATE_PREAUTHORIZED, }, }, [STATE_INQUIRY]: { on: { - [TRANSITION_REQUEST_PAYMENT_AFTER_INQUIRY]: STATE_PENDING_PAYMENT, + [TRANSITION_REQUEST_PAYMENT_AFTER_INQUIRY]: STATE_PREAUTHORIZED, }, }, - - [STATE_PENDING_PAYMENT]: { - on: { - [TRANSITION_EXPIRE_PAYMENT]: STATE_PAYMENT_EXPIRED, - [TRANSITION_CONFIRM_PAYMENT]: STATE_PREAUTHORIZED, - }, - }, - - [STATE_PAYMENT_EXPIRED]: {}, [STATE_PREAUTHORIZED]: { on: { [TRANSITION_DECLINE]: STATE_DECLINED, ``` ### Remove Stripe checks from EditListingWizard handlePublishListing function In [`EditListingWizard.js`](https://github.com/sharetribe/web-template/blob/main/src/containers/EditListingPage/EditListingWizard/EditListingWizard.js) we check if a provider has a Stripe account with all the required information before we allow them to publish listings. This check is done in the `handlePublishListing` function. If you are removing all Stripe functionality from your marketplace, you can simplify the whole function like this, and remove the unused code. ```js filename="EditListingWizard.js" handlePublishListing(id) { this.props.onPublishListingDraft(id); } ``` If you are adding an option to not use Stripe, for example a listing type where you handle payments outside the Sharetribe transaction process, you can instead add a new variable such as `isNoStripeProcess` and use it to bypass the Stripe requirement while keeping it for other listing types. ```diff filename="EditListingWizard.js" handlePublishListing(id) { const { onPublishListingDraft, currentUser, stripeAccount, listing, config } = this.props; const processName = listing?.attributes?.publicData?.transactionProcessAlias.split('/')[0]; const isInquiryProcess = processName === INQUIRY_PROCESS_NAME; + const isNoStripeProcess = processName === BOOKING_NO_STRIPE_PROCESS_NAME; /* ... */ if ( isInquiryProcess || + isNoStripeProcess || !isPayoutDetailsRequired || (stripeConnected && !stripeRequirementsMissing) ) { onPublishListingDraft(id); } else { this.setState({ draftId: id, showPayoutDetails: true, }); } } ``` After changing the `handlePublishListing` you can remove unused code. You also might want to clean up `EditListingPage` and remove the Stripe related props we pass to `EditListingWizard`. ### Remove Stripe checks from ListingPageCarousel and ListingPageCoverPhoto The listing page components show a warning to listing authors if they need to add their Stripe payout details to start accepting orders. With a non-Stripe listing type, this logic needs to be modified in both files. ```diff filename="ListingPageCarousel.js" + const isNoStripeProcess = processName === BOOKING_NO_STRIPE_PROCESS_NAME; const currentAuthor = authorAvailable ? currentListing.author : null; const ensuredAuthor = ensureUser(currentAuthor); const authorNeedsPayoutDetails = - ['booking', 'purchase'].includes(processType) || (isNegotiation && unitType === OFFER); + (!isNoStripeProcess && ['booking', 'purchase'].includes(processType)) || + (isNegotiation && unitType === OFFER); const noPayoutDetailsSetWithOwnListing = isOwnListing && (authorNeedsPayoutDetails && !currentUser?.attributes?.stripeConnected); const payoutDetailsWarning = noPayoutDetailsSetWithOwnListing ? ( ) : null; ``` ### Remove Stripe checks from MakeOfferForm If you are fully removing Stripe, or if you are removing Stripe payments from a process that is based on `default-negotiation`, you also need to modify the [MakeOfferForm.js](https://github.com/sharetribe/web-template/blob/main/src/containers/MakeOfferPage/MakeOfferForm/MakeOfferForm.js). This component has a limitation that providers cannot submit their offer if they don't have a Stripe account. You will need to remove the references to `stripeConnected` in the form to remove this restriction. ### Edit CheckoutPage Removing Stripe from `CheckoutPage` is probably the most complicated task in this list because the Strong Customer Authentication (SCA) and ability to save payment method have added a lot of logic to the current page. By default, the `CheckoutPage` component uses two sub-components, depending on the transaction process: - _CheckoutPageWithInquiryProcess_ is used with the default free inquiry process - _CheckoutPageWithPayment_ is used with the default booking and purchase processes We can use an edited version of the _CheckoutPageWithPayment_ component, titled _CheckoutPageWithoutPayment_, as a starting point for the modifications. - [CheckoutPageWithoutPayment.js](/resources/cookbook-assets/CheckoutPageWithoutPayment.js) The `CheckoutPageWithoutPayment.js` component uses the default transition variables REQUEST_PAYMENT and REQUEST_PAYMENT_AFTER_INQUIRY in the `fetchSpeculatedTransactionIfNeeded` function, so if you have edited your transition names, double check that this function calls the right transitions. The _CheckoutPageWithoutPayment_ component also imports a _SimpleOrderForm_ component. Let's add the component to the existing StripePaymentForm folder. - [SimpleOrderForm.js](/resources/cookbook-assets/SimpleOrderForm.js) If you are removing Stripe entirely, you can replace the `CheckoutPageWithPayment` component with the `CheckoutPageWithoutPayment` one on CheckoutPage.js. If you are adding a separate transaction process that works without Stripe, you will need to add the `CheckoutPageWithoutPayment` to the CheckoutPage `return` statement to take it into use. ```diff filename="CheckoutPage.js" + const isNoStripeProcess = processName === BOOKING_NO_STRIPE_PROCESS_NAME; return processName && isInquiryProcess ? ( + ) : processName && isNoStripeProcess && !speculateTransactionInProgress ? ( + ) : processName && !isInquiryProcess && !speculateTransactionInProgress ? ( ```jsx filename="CheckoutPageTransactionHelpers.js" export const processCheckoutWithoutPayment = ( orderParams, extraParams ) => { const { message, onInitiateOrder, onSendMessage, pageData, process, setPageData, sessionStorageKey, } = extraParams; const storedTx = ensureTransaction(pageData.transaction); const processAlias = pageData?.listing?.attributes?.publicData?.transactionProcessAlias; //////////////////////////////////////////////// // Step 1: initiate order // // by requesting booking from Marketplace API // //////////////////////////////////////////////// const fnRequest = (fnParams) => { // fnParams should be { listingId, deliveryMethod, quantity?, bookingDates?, protectedData } const requestTransition = storedTx?.attributes?.lastTransition === process.transitions.INQUIRE ? // ===================== // Double check that these transition names match the // transition names in your transaction process! // ===================== process.transitions.REQUEST_PAYMENT_AFTER_INQUIRY : process.transitions.REQUEST_PAYMENT; const isPrivileged = process.isPrivileged(requestTransition); const orderPromise = onInitiateOrder( fnParams, processAlias, storedTx.id, requestTransition, isPrivileged ); orderPromise.then((order) => { // Store the returned transaction (order) persistTransaction( order, pageData, storeData, setPageData, sessionStorageKey ); }); return orderPromise; }; ////////////////////////////////// // Step 2: send initial message // ////////////////////////////////// const fnSendMessage = (fnParams) => { const orderId = fnParams?.id; return onSendMessage({ id: orderId, message }); }; ///////////////////////////////// // Call each step in sequence // //////////////////////////////// return fnRequest(orderParams).then((res) => fnSendMessage({ ...res }) ); }; ``` - Import and use _loadInitialData_ instead of _loadInitialDataForStripePayments_ in _CheckoutPage.js_. ```diff filename="CheckoutPage.js" // Do not fetch extra data if user is not active (E.g. they are in pending-approval state.) if (isUserAuthorized(currentUser)) { // This is for processes using payments with Stripe integration if (getProcessName(data) !== INQUIRY_PROCESS_NAME) { // Fetch StripeCustomer and speculateTransition for transactions that include Stripe payments loadInitialDataForStripePayments({ pageData: data || {}, fetchSpeculatedTransaction, - fetchStripeCustomer, config, }); } } ``` ### Hide or remove Stripe related pages If you are not using Stripe at all, you should remove or at least hide the pages that are meant for managing the information saved to Stripe. These pages are [StripePayoutPage](https://github.com/sharetribe/web-template/tree/main/src/containers/StripePayoutPage) and [PaymentMethodsPage](https://github.com/sharetribe/web-template/tree/main/src/containers/PaymentMethodsPage). Both of these pages are accessible through the account settings so we want to hide them from both navigation component and route configuration. To hide the pages from account settings, modify the `LayoutWrapperAccountSettingsSideNav` component: ```diff filename="LayoutWrapperAccountSettingsSideNav.js" - const { currentPage, showPaymentMethods, showPayoutDetails } = accountSettingsNavProps; + const { currentPage } = accountSettingsNavProps; - const payoutDetailsMaybe = showPayoutDetails - ? [ - { - text: , - selected: currentPage === 'StripePayoutPage', - id: 'StripePayoutPageTab', - linkProps: { - name: 'StripePayoutPage', - }, - }, - ] - : []; + const payoutDetailsMaybe = []; - const paymentMethodsMaybe = showPaymentMethods - ? [ - { - text: ( - - ), - selected: currentPage === 'PaymentMethodsPage', - id: 'PaymentMethodsPageTab', - linkProps: { - name: 'PaymentMethodsPage', - }, - }, - ] - : []; + const paymentMethodsMaybe = []; ``` In `routeConfiguration.js`, remove references to `StripePayoutPage` and `PaymentMethodsPage`. ```diff filename="routeConfiguration.js" export const ACCOUNT_SETTINGS_PAGES = [ 'ContactDetailsPage', 'PasswordChangePage', - 'StripePayoutPage', - 'PaymentMethodsPage', 'ManageAccountPage' ]; return [ /* ... */ - { - path: '/account/payments', - name: 'StripePayoutPage', - auth: true, - authPage: 'LoginPage', - component: StripePayoutPage, - loadData: pageDataLoadingAPI.StripePayoutPage.loadData, - }, - { - path: '/account/payments/:returnURLType', - name: 'StripePayoutOnboardingPage', - auth: true, - authPage: 'LoginPage', - component: StripePayoutPage, - loadData: pageDataLoadingAPI.StripePayoutPage.loadData, - }, - { - path: '/account/payment-methods', - name: 'PaymentMethodsPage', - auth: true, - authPage: 'LoginPage', - component: PaymentMethodsPage, - loadData: pageDataLoadingAPI.PaymentMethodsPage.loadData, - }, ``` Finally, you need to remove StripePayoutPage from the ModalMissingInformation.js component whitelist. ```diff filename="ModalMissingInformation.js" const MISSING_INFORMATION_MODAL_WHITELIST = [ 'LoginPage', 'SignupPage', 'ContactDetailsPage', 'EmailVerificationPage', 'PasswordResetPage', - 'StripePayoutPage', ]; ``` ### Extra: Removing unused exports This is not a mandatory step to do but if you want to clean up the code you can go through the `components/index.js` and `containers/index.js`, and remove the Stripe components. After this, you should go through the whole codebase and remove all the references to these components. We have tried to follow the naming pattern where Stripe related components have `Stripe` in their name but there are a couple of exceptions like `PaymentMethodsPage`. All these components should be removed also from `examples.js`. Stripe related components: - StripeBankAccountTokenInputField - StripeConnectAccountStatusBox - StripePaymentAddress - StripePayoutPage - PaymentMethodsPage - PaymentMethodsForm - StripePaymentForm - StripeConnectAccountForm ## Removing payments If you want to remove payments completely from your marketplace, there are even more things to consider. Again it depends a lot on your use-case what kind of changes are needed. If you e.g. want to add a transaction process with free bookings or purchases, you need to build the conditional logic for showing pricing components when needed and hiding them when not. If you want to build the marketplace completely without payments, you most likely want to hide all the references to listing price. ### More changes to the transaction process Remove the actions related to payments, e.g. `:action/privileged-set-line-items` and `:action/calculate-full-refund` in the default processes. Read more about actions in the [transaction process actions reference article.](/references/transaction-process-actions/) Remember to also update email templates that contain pricing information. ### Edit API calls in client app's server You will need to edit the API calls in your client app's server. If there are no payments used, there is most likely no need for calculating line items, so we can remove `transactionLineItems` function calls from API endpoints. Depending on your use case, you may or may not want to use the privileged transitions at all. However, in case you are updating e.g. transaction protected data, it is a good practice to do that safely in the template's backend. ### Other code changes Especially if you are removing the payment completely from your marketplace, you should also go through all these pages to hide or remove the parts related to payments. - ListingPage, CheckoutPage, TransactionPage: Remove or hide `OrderBreakdown` component when the transaction is free - EditListingPage: remove pricing tab from `EditListingWizard` - SearchPage: remove price filter & edit `ListingCard` so that it doesn't show the listing price --- ## Create custom search filters in Sharetribe Web Template Path: how-to/search/change-search-filters-in-template/index.mdx # Create custom search filters in Sharetribe Web Template The search experience can be improved by adding search filters to narrow down the results. The filters rely on listing indexed data. There are 3 different UI contexts that render filters. On the mobile layout, all filters are rendered to a modal inside the `SearchFiltersMobile` component. On the desktop layout, the most important filters are in `SearchFiltersPrimary`, and extra filters are in `SearchFiltersSecondary` panel, which opens when user clicks the _"More filters"_ button. ## Existing filter types Sharetribe Web Template has several different filter types by default: _BookingDateRangeFilter_, _KeywordFilter_, _PriceFilter_, _SelectSingleFilter_ and _SelectMultipleFilter_. Select single and select multiple filters are generic – they can be used to filter search results using different kinds of data. The price and date range filters, on the other hand, are only used for filtering by price and date range. Listings with hourly bookings can also be filtered by their availability in a date range with an optional minimum duration. Keyword filter is a special case - more about that later. _SelectSingleFilter_ and _SelectMultipleFilter_ can be used with extended data. The _SelectSingleFilter_ can be used to filter out listings with only one string value in a related public data field. For instance, a listing's publicData attribute could contain an attribute `condition: 'new'`. The related Marketplace API listing query could then be made with the query parameter `pub_condition=new`. The _SelectMultipleFilter_, on the other hand, can take multiple values for a single search parameter. In this case, a listing entity could contain public data `accessories: ['bell', 'lights', 'lock']` and the query parameter to retrieve that listing among other search results could be `pub_accessories=has_any:bell,mudguard`. ## Keyword filter The keyword filter works a bit differently from the other filters. It does filter search results, but it also sorts those results according to how strongly the listing's data (title, description, and possible extended data) correlates with the search string. Currently, there is no decay function that would map keyword match correlation with distance to `origin`, so the _origin_ param cannot be used at the same time as the _keyword_ param. You can read more about how the keyword search works in the related [concepts article](/concepts/listings/how-the-listing-search-works/). Because descriptions can be toggled off as a requirement per listing type, some listings may have 'hidden' descriptions if this setting changed after the listing was created. The description is still indexed for search and factored into the relevance score. ![Desktop filters](./keyword-search.png) It is possible to remove location search from topbar and replace it with the keyword search or use them together (without origin param). You can configure the main search type in **src/config/configSearch.js**. Search strings with only 1 or 2 letters have a longer timeout before the search query is made. ## Creating your own filter types If you are creating new filter components, note that we are using two different types of components: **popup** and **plain**. Popup components are rendered as primary dropdowns in the map search view in `SearchFiltersPrimary` component. Plain components are used on the grid search page and `SearchFiltersMobile`, and with `SearchFiltersSecondary` on the map search page. _SearchFiltersSecondary_ opens secondary filters in a distinct panel in order to fit additional filters to the desktop search view. To make creating new filters easier, there are two generic components: `FilterPopup` and `FilterPlain`. These components expect that you give form fields as child component. This is a simplified example of how the FilterPlain and FilterPopup components are used in [SelectMultipleFilter.js](https://github.com/sharetribe/web-template/blob/main/src/containers/SearchPage/SelectMultipleFilter/SelectMultipleFilter.js): ```jsx filename="SelectMultipleFilter.js" return showAsPopup ? ( ) : ( ); ``` When you have your custom filter component ready, you need to add it to **SearchPage/FilterComponent.js**: ```shell └── src └── containers └── SearchPage └── FilterComponent.js ``` --- ## Manage search schemas with Sharetribe CLI Path: how-to/search/manage-search-schemas-with-sharetribe-cli/index.mdx # Manage search schemas with Sharetribe CLI Sharetribe CLI (Command-line interface) is a tool for changing your marketplace's advanced configurations such as transaction processes and email templates. This guide expects that you have already installed Sharetribe CLI and are logged in with your API key. If not, it's recommended to first read the guide [Getting started with Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/). In this guide, we will add a data schema for the `accessories` public data field in listings. If you have a listing field defined in the template code, you need to define a search schema for the attribute. We will also see how to manage data schema for user profiles and transactions. Those schemas are not required for Sharetribe Web Template to work, but can be useful when customizing your template, or when building own integrations via the Sharetribe Integration API. ## Extended data types and schema scopes There are [various types of extended data](/references/extended-data/). Search schema is scoped to a particular type of extended data and the support differs depending on whether the schema is defined for listings or user profiles. The following table summarizes the supported search schema scopes. | Schema for | Supported scopes | | ----------- | ------------------------------------ | | listing | public, metadata | | userProfile | public, private, protected, metadata | | transaction | protected, metadata | There is no API endpoint for querying users in the Marketplace API, so `userProfile` search schema applies only to the [/users/query endpoint in the Integration API](https://www.sharetribe.com/api-reference/integration.html#query-users). All types of extended data except transaction protected data are editable in Console by the operator. Only listing and user public data and metadata can be seen by other marketplace users. To see more details about extended data, see the [Extended data](/references/extended-data/) reference. You can store any JSON data in extended data, but only top-level keys of certain type can have search schemas. If there is a mismatch between the defined schema and what is stored to the extended data, the indexing just skips those values. ## Schema types and cardinalities | Type | Cardinality | Example data | Example query | | ---------- | ----------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | | enum | one | `condition: "new"` | `pub_condition=new,used` | | multi-enum | many | `accessories: ["bell", "lights"]` | `pub_accessories=has_all:bell,lights` or `pub_accessories=has_any:bell,lights` | | boolean | one | `hasPannierRack: true` | `pub_hasPannierRack=true` | | long | one | `manufactureYear: 2021` | `pub_manufactureYear=2020,2023` | | text | one | `accessoriesDescription: "Pannier bags and a dog carrier (max 18 pounds) available on request."` | `keywords=bags%20dog%20carrier` | Data schema of type `text` is currently only supported for listings. Data schemas of type `enum` or `multi-enum` do not require defining an acceptable enum of values for search with the CLI. Here, `enum` denotes that the match has to be exact, unlike with type `text` which returns partial matches. In practice, creating a search functionality with `enum` or `multi-enum` fields often includes offering a pre-set enum of options for filtering in the client, instead of accepting user-entered values. Note that the scope in the examples above is `public`. Please use the correct prefix depending on the scope of the data (`meta_` for metadata, `priv_` for private data, `prot_` for protected data and `pub_` for public data). Also, it's worth noting that the query parameter with a `text` schema is `keywords` which also targets the `title` and `description` attributes of a listing. This query parameter is only supported in listing queries. See [Keyword search](/concepts/listings/how-the-listing-search-works/#keyword-search) for more information. ### Providing multiple query params for a single field You can provide multiple values in the query parameter by separating those with a comma. The matching behavior is different for different schema types. With the `enum` type like the one above, when you query `pub_condition=used,new`, you will match listings with either "used" OR "new" as the condition. With the `multi-enum`, you can control the matching mode explicitly. The query `pub_accessories=has_all:bell,lights` will match listings with "bell" AND "lights" in the accessories whereas the query `pub_accessories=has_any:bell,lights` will match listings with either "bell" OR "lights" (or both). If you don't specify the match mode in the query (i.e. `pub_accessories=bell,lights`), by default we use the has_all matching mode (AND) for multi enums. With the `text` type, you provide a search query, so splitting values with a comma doesn't make sense. You will just provide a string of text as the search query, and the query will be used as described [in the keyword search explanation](/concepts/listings/how-the-listing-search-works/#keyword-search) section. With the `long` type, you can provide minimum and/or maximum values for the filtering. For the full query reference, see the [/listings/query](https://www.sharetribe.com/api-reference/marketplace.html#query-listings) endpoint API reference. ## Listing fields and search settings in Sharetribe Console When you add listing fields in Sharetribe Console, and select _Include this field in keyword search_ or _Add a filter to Search Page_, Sharetribe generates a search schema for the field automatically. In Console, the available field type options are - free text (search schema: `text`) - number (search schema: `long`) - select one (search schema: `enum`) - select multiple (search schema: `multi-enum`) When you review your search schemas with the `flex-cli search` command, schemas defined for listing fields in console have the following doc string in the search schema: ```shell A listing field defined in Console. Can not be edited with the CLI. ``` If you then try to add a search schema through Sharetribe CLI for a key and scope that already exists in a Console-originated listing field, you will see the following error: ``` › API call failed. Status: 409, reason: A search schema with the same key and scope has already been defined in an asset. ``` ## Adding listing search schemas Through Console, you can create a multi-enum (select multiple) listing field with `has_all` search mode. If you want to have a multi-enum field with `has_any` logic, you need to define that listing field [in your local configurations](/how-to/listings/extend-listing-data-in-template/). For a locally defined listing field, you need to add a listing search schema manually through the CLI. We will create a search schema for the public data attribute `accessories` to make this kind of filtering possible. Our marketplace has one Console-created listing field, `condition`. Let's first see how that search schema looks: ``` $ flex-cli search -m my-marketplace-dev Schema for Scope Key Type Default value Doc listing public condition enum A listing field defined in Console. Can not be edited with the CLI. ``` Let's add the search schema for `accessories`: ``` $ flex-cli search set --key accessories --type multi-enum --scope public -m my-marketplace-dev ``` We should now see the details for this new schema alongside our Console created one: ``` $ flex-cli search -m my-marketplace-dev Schema for Scope Key Type Default value Doc listing public condition enum A listing field defined in Console. Can not be edited with the CLI. listing public accessories multi-enum ``` Note that `--schema-for` option is not needed when adding schema for `listing` as `listing` is the default. If you wish to remove a schema, you can use the `search unset` command. If you have already defined a search schema for a key through Sharetribe CLI, you can not create a listing field in Sharetribe Console with that same key. ![Console warning about conflicting keys](./conflictingSearchSchema.png) ## Adding user search schema Adding user search schemas is only supported in Sharetribe CLI versions 1.10.0 and above. Use yarn to update Sharetribe CLI by running `yarn global upgrade flex-cli` or `npm update -g flex-cli` if you are using npm. User profile search schema can be useful, if you have an Integration API application that needs to query different sets of users, depending on some value in the user profile's extended data. For instance, if users have `age` attribute stored in their protected data, you can use the [/users/query endpoint in the Integration API](/references/extended-data/) to find users of a certain age range. Search schema for user profiles can be added as follows: ``` $ flex-cli search set --schema-for userProfile --key age --type long --scope protected -m my-marketplace-dev ``` The above adds a search schema for `userProfile` with `long` type for a `key` named "age". Querying the defined schemas now shows both the listing schemas added on the previous step and the new user profile schema: ``` $ flex-cli search -m my-marketplace-dev Schema for Scope Key Type Default value Doc listing public condition enum A listing field defined in Console. Can not be edited with the CLI. listing public accessories multi-enum userProfile protected age long ``` If you wish to remove a schema, you can use the `search unset` command. ## Adding transaction search schema Adding user search schemas is only supported in Sharetribe CLI versions 1.14.0 and above. Use yarn to update Sharetribe CLI by running `yarn global upgrade flex-cli` or `npm update -g flex-cli` if you are using npm. Transaction search schema can be useful, if you are building features where for example providers and customers can filter through different kinds of transactions. For example, you could have a separate views for shipping transactions and pickup transactions. Search schema for transactions can be added as follows: ``` $ flex-cli search set --schema-for transaction --key deliveryMethod --type enum --scope protected -m my-marketplace-dev ``` The above adds a search schema for `transaction` with `enum` type for a `key` named "deliveryMethod". Querying the defined schemas now shows both the listing schemas added on the previous step and the new user profile schema: ``` $ flex-cli search -m my-marketplace-dev Schema for Scope Key Type Default value Doc listing public condition enum A listing field defined in Console. Can not be edited with the CLI. listing public accessories multi-enum transaction protected deliveryMethod enum userProfile protected age long ``` If you wish to remove a schema, you can use the `search unset` command. ## Adding a search schema with a default value Sometimes, you may want to query entities that do not have a certain attribute set. For instance, you may have promoted listings on your marketplace, labelled with a metadata attribute `isPromoted: true`. If you only have a handful of promoted listings, you likely do not want to tag all other listings with `isPromoted: false`. Instead, Sharetribe allows you to set a default value for the search schema – all listings (or users, or transactions) that do not have the attribute get returned when querying the default value. You can set the default value for a search schema of any entity type by passing a `--default` flag with the desired default value. To create a listing search schema described above, the Sharetribe CLI command is as follows: ``` $ flex-cli search set --key isPromoted --type boolean --scope metadata --default false -m my-marketplace-dev ``` Now, if we query all the search schemas on the marketplace, we can see the default value for the `isPromoted` schema in the corresponding column. ``` $ flex-cli search -m my-marketplace-dev Schema for Scope Key Type Default value Doc listing metadata isPromoted boolean false listing public condition enum A listing field defined in Console. Can not be edited with the CLI. listing public accessories multi-enum transaction protected deliveryMethod enum userProfile protected age long ``` ## Summary In this guide, we used Sharetribe CLI to define search schemas for our marketplace. We also saw how schemas defined through Sharetribe Console and Sharetribe CLI interact. We used the public data attributes `condition` and `accessories` as examples. In addition, we looked at adding user search schemas for Integration API, adding a transaction schema, and adding a listing schema with a default value. For more information, see the following resources: - [How the search works](/concepts/listings/how-the-listing-search-works/) background article - [Extended data reference](/references/extended-data/) - API reference for the [/listings/query](https://www.sharetribe.com/api-reference/marketplace.html#query-listings) endpoint in the Marketplace API - API reference for the [/listings/query](https://www.sharetribe.com/api-reference/integration.html#query-listings) endpoint in the Integration API - API reference for the [/users/query](https://www.sharetribe.com/api-reference/integration.html#query-users) endpoint in the Integration API - [Extend listing data in template](/how-to/listings/extend-listing-data-in-template/) - [Change search filters in template](/how-to/search/change-search-filters-in-template/) --- ## Add transaction fields to a custom listing type Path: how-to/transaction-process/add-transaction-fields/index.mdx # Add transaction fields to a custom listing type Starting in February 2026, you can configure transaction fields for your listing types in Console. Transaction fields are listing type specific. If you have configured your own custom listing types in code by [following these instructions](/how-to/transaction-process/change-transaction-process-in-template/#add-a-listing-type-that-uses-the-transaction-process), any transaction fields for those listing types will also need to be defined in code. This article illustrates how you can define transaction fields for your custom listing types. ## Add transaction fields array A listing type can optionally have a `transactionFields` array that consists of transaction field definitions. The full configuration looks like this: ```jsx filename="src/config/configListing.js" export const listingTypes = [ { listingType: 'booking-with-negotiation', label: 'Booking with negotiation', transactionType: { process: 'booking-with-negotiation', alias: 'booking-with-negotiation/release-1', unitType: 'night', }, transactionFields: [ { key: 'requests', label: 'Extra requests for the hosts', schemaType: 'text', showTo: 'customer', }, { label: 'How many people are staying at the venue', key: 'peopleStaying', schemaType: 'long', showTo: 'customer', numberConfig: { minimum: 1, maximum: 10, }, saveConfig: { required: true, }, }, { key: 'schedulePreference', label: 'Schedule preference', schemaType: 'enum', showTo: 'customer', enumOptions: [ { label: 'Morning cleanup (10am-12am)', option: 'morning', }, { label: 'Afternoon cleanup (2pm-4pm)', option: 'afternoon', }, ], }, { key: 'dietaryPreferences', label: 'Dietary preferences', schemaType: 'multi-enum', showTo: 'customer', enumOptions: [ { label: 'Vegetarian', option: 'vegetarian', }, { label: 'Vegan', option: 'vegan', }, { label: 'Gluten free', option: 'glutenFree', }, { label: 'No caffeine', option: 'decaf', }, { label: 'Nut free', option: 'nutFree', }, { label: 'Dairy free', option: 'dairyFree', }, ], }, ], }, ``` Transaction fields need to be defined, at minimum, by the attributes **key**, **label**, **schemaType**, and **showTo**. ```jsx { key: 'requests', label: 'Extra requests for the hosts', schemaType: 'text', showTo: 'customer', }, ``` The **schemaType** attribute determines the shape of the data being saved: - **enum** attributes are saved as a single string value from a list of predefined options - **multi-enum** attributes are saved as an array of string values from a list of predefined options - **boolean** attributes are saved as **true** or **false** boolean values - **long** attributes are saved as long i.e. as an 8-byte whole number - **shortText** attributes are saved as a single short-text entry e.g. a URL - **text** attributes are saved as a single text entry - **youtubeVideoUrl** attributes are saved as a single text entry and displayed in the Sharetribe Web Template as an embedded Youtube video component The **showTo** attribute determines the participant of the transaction from whom the fields are collected. For fields with `schemaType: enum` or `schemaType: multi-enum`, you need to define an **enumOptions** attribute. The `enumOptions` attribute is an array of objects, and each object needs to define **option** and **label** attributes. ```jsx { key: 'schedulePreference', label: 'Schedule preference', schemaType: 'enum', showTo: 'customer', enumOptions: [ { label: 'Morning cleanup (10am-12am)', option: 'morning', }, { label: 'Afternoon cleanup (2pm-4pm)', option: 'afternoon', }, ], }, ``` For fields with `schemaType: long`, you can optionally define a **numberConfig** attribute with `minimum` and `maximum` attributes. If the field has a `numberConfig` attribute defined, the Sharetribe Web Template validates the input against those boundaries. For all fields, you can optionally define a **saveConfig** attribute to set the field as required. ```jsx { label: 'How many people are staying at the venue', key: 'peopleStaying', schemaType: 'long', showTo: 'customer', numberConfig: { minimum: 1, maximum: 10, }, saveConfig: { required: true, }, }, ``` ## Verify where the fields are collected and displayed for your process Transaction fields are collected in different places according to the transaction process. - `default-inquiry`: transaction fields with `showTo: 'customer'` are collected on CheckoutPageWithInquiryProcess page. Fields with `showTo: 'provider'` are not collected. - `default-booking` and `default-purchase`: transaction fields with `showTo: 'customer'` are collected on CheckoutPageWithPayment. Fields with `showTo: 'provider'` are not collected. - `default-negotiation`: transaction fields with `showTo: 'customer'` are collected on RequestQuotePage, and transaction fields with `showTo: 'provider'` are collected on the MakeOfferPage. Transaction fields are displayed on the TransactionPage. The page passes the prop `transactionFieldsComponent` to `TransactionPanel`, `RequestQuote`, and `Offer` components, and those components then display the transaction fields in the correct context. If you are using a custom transaction process with your custom listing type, you will need to review the logic that defines how transaction fields are collected and displayed. Depending on the process, you may need to make changes. --- ## Change transaction process in Sharetribe Web Template Path: how-to/transaction-process/change-transaction-process-in-template/index.mdx # Change transaction process in Sharetribe Web Template Sharetribe Web Template defines four transaction processes by default: - daily, nightly, hourly, and fixed bookings use the **default-booking** process, - product sales use the **default-purchase** process - price negotiations use the **default-negotiation** process, and - inquiries use the **default-inquiry** process. The template is created to support states and transitions defined in those processes. How the transaction process works underneath the Marketplace API depends on how your process is customised in our backend. To customise the transaction process in the backend, you should use Sharetribe CLI. See the [Getting started with Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/) tutorial to get familiar with the tool. If you have changed the transaction process in your marketplace, or added a new one to use in parallel with the existing ones, you should check if your client app needs to be updated to match this different transaction process. You can read more about how these processes work from a background info article about the [transaction process](/concepts/transactions/transaction-process/). The following guide will help you to customise the process flow in the template to match the process in our backend. This guide assumes that you have already followed these instructions to add a new process: - [Create a new transaction process with the Sharetribe CLI](/how-to/transaction-process/create-new-transaction-process-with-cli/). ## Add a listing type that uses the transaction process The [src/config/configListing.js](https://github.com/sharetribe/web-template/blob/main/src/config/configListing.js) file lists the listing types actively used by the template, as well as the transaction processes related to those types. You need to add a listing type configuration for a new transaction process into the `listingTypes` array to use it in the template. If you have not added any listing types in code before, the entries in the listing type array are all commented out by default. They serve as examples of what the listing type configuration looks like. To add a new listing type, you can either replace or comment out any previously active process definitions (if you only want to use the new process) or leave them as they are (if you want to allow using multiple processes in the same application). ```js filename="src/config/configListing.js" export const listingTypes = [ { listingType: 'booking-with-negotiation', label: 'Booking with negotiation', transactionType: { process: 'booking-with-negotiation', alias: 'booking-with-negotiation/release-1', unitType: 'night', }, }, // { // listingType: 'daily-booking', // label: 'Daily booking', // transactionType: { // process: 'default-booking', // alias: 'default-booking/release-1', // unitType: 'day', // }, // availabilityType: 'oneSeat', // defaultListingFields: { // location: true, // payoutDetails: true, // }, // }, /* ... */ ]; ``` Settings configured in local configurations files are overridden by any fetched via the Asset Delivery API. When you first add a new listing type in the configListing.js file, you need to follow the steps in [this article](/template/configuration/hosted-and-local-configurations/) to modify the way your template merges local and hosted configurations. The `alias` variable should point to the correct alias. You need to check in Console which process and process version your client app should support. All available transaction process aliases can be found in the [Build section](https://console.sharetribe.com/advanced/transaction-processes) in Console. ![Process alias in Console](./booking-with-negotiation-showing-alias.png) The `unitType` specifies what kind of units the web app is dealing with when handling the listing type. The Sharetribe Web Template recognises and handles these unit types by default: - **day**, **night**, **hour**, and **fixed** for the **default-booking** process - **item** for the **default-purchase** process - **inquiry** for the **default-inquiry** process - **request** and **offer** for the **default-negotiation** process. The Sharetribe engine can handle other unit types besides the default ones. If you use a unit type outside the defaults, you need to add custom handling for it in e.g. line item calculation, order handling, and email templates. ## Update the relevant files in src/transactions folder Supported transaction processes are also defined in the files found in `src/transactions` folder. In all cases, you will need to update the `transaction.js` file to include your new process definition. ```jsx filename="src/transactions/transaction.js" // Then names of supported processes export const PURCHASE_PROCESS_NAME = 'default-purchase'; export const BOOKING_PROCESS_NAME = 'default-booking'; export const INQUIRY_PROCESS_NAME = 'default-inquiry'; export const NEGOTIATION_PROCESS_NAME = 'default-negotiation'; // Add new processes with a descriptive name export const NEGOTIATED_BOOKING_PROCESS_NAME = 'booking-with-negotiation'; ``` In addition to updating the process name to your `transaction.js` file, you will need to make sure the application has an accurate representation of the different transitions and states in your new transaction process. The transitions and states for the existing processes are defined in the `transactionProcess....js` files in the same `src/transactions` folder. If you are replacing one of the default processes (for instance the default booking process with a new booking process with different transitions and states), you can modify the existing **transactionProcessBooking.js** file to correspond to the new process. If you are creating a parallel new process and want to allow listing authors to choose between two processes for their listings, you will need to create a new transaction process file and import it in `transaction.js`. ### Determine which process files to work with The best way to know which `transactionProcess...js` file to use as the basis of your new one is to compare the transaction process graphs of the two processes in the Console > Build > Advanced > Transaction process visualiser view. If two processes have mostly identical states and transitions with only one or two exceptions, you can use the existing files of that process as the basis for your new one with very little modification. The more differences there are between two processes, the more you need to customize the files. We recommend that you use the files of the existing process that is closest to your new process. For example, the image below shows a comparison between Sharetribe example negotiated booking process and the default-booking process. You can see that the main difference between the two flows is the pricing and payment flow – in the default, the customer first pays and then the provider either accepts or declines, whereas in the negotiated booking flow, the parties first need to accept the suggested price and payment happens after that. This means that the files related to the `default-booking` process are a good starting point for implementing this guide for that specific process. ![Comparison between negotiated booking and default booking](./default-booking-negotiated-booking-comparison.png) The more your new process differs from existing processes, the more custom changes you will need to make to the presentational components and containers. You might have use cases in your transaction process that are not currently covered by the existing components and containers. The following instructions specify the steps for modifying the existing `transactionProcessBooking.js` file. If you do create a new one, we recommend you replicate the existing default process file and make the necessary changes instead of creating one from scratch. ### Update transitions and states If the new transaction process has different transitions and states, you should add (or remove) those. Transition names need to exactly match the transitions used in Marketplace API, since transitions are part of queried transaction entities. **Keeping transition names unique across all your transaction processes.** Two parts of the Template rely on transition names without also checking which process a transaction belongs to: - `server/api-util/negotiation.js`: helper functions such as `isIntentionToMakeOffer` check the transition name only. If a different process has a transition that shares a name with a transition from `default-negotiation`, offer-handling logic will trigger incorrectly for that process. - `user.duck.js`: the inbox notification count makes two API queries (one per user role) based on state names collected from all registered processes. Duplicate state names across processes, where a "needs attention" state in one process shares a name with a neutral state in another, will cause inaccurate notification counts. If you modify either file to support a new process, guard against the above by checking the process name alongside the transition or state name. Note: transitions that work identically in every process (such as review transitions) can safely share a name across processes. You can find the names of your transaction process transitions and states by viewing your process in Console > Build > Advanced > Transaction process visualiser. ![The booking with negotiation process in Console](./booking-with-negotiation-and-alias-in-console.png) ```diff filename="transactionProcessBooking.js" export const transitions = { // When a customer makes a booking to a listing, a transaction is // created with the initial request-payment transition. // At this transition a PaymentIntent is created by Marketplace API. // After this transition, the actual payment must be made on client-side directly to Stripe. - REQUEST_PAYMENT: 'transition/request-payment', + REQUEST: 'transition/request', // A customer can also initiate a transaction with an inquiry, and // then transition that with a request. INQUIRE: 'transition/inquire', - REQUEST_PAYMENT_AFTER_INQUIRY: 'transition/request-payment-after-inquiry', + REQUEST_AFTER_INQUIRY: 'transition/request-after-inquiry', + PROVIDER_OFFER: 'transition/provider-offer', /* ... */ EXPIRE_CUSTOMER_REVIEW_PERIOD: 'transition/expire-customer-review-period', EXPIRE_PROVIDER_REVIEW_PERIOD: 'transition/expire-provider-review-period', EXPIRE_REVIEW_PERIOD: 'transition/expire-review-period', } export const states = { INITIAL: 'initial', INQUIRY: 'inquiry', + CUSTOMER_MADE_OFFER: 'customer-made-offer', + PROVIDER_MADE_OFFER: 'provider-made-offer', PENDING_PAYMENT: 'pending-payment', PAYMENT_EXPIRED: 'payment-expired', - PREAUTHORIZED: 'preauthorized', DECLINED: 'declined', ACCEPTED: 'accepted', EXPIRED: 'expired', CANCELED: 'canceled', DELIVERED: 'delivered', REVIEWED: 'reviewed', REVIEWED_BY_CUSTOMER: 'reviewed-by-customer', REVIEWED_BY_PROVIDER: 'reviewed-by-provider', }; ``` ### Add or update state graph to match the new transaction process State graph description makes it easier to understand how the transaction process works - but even more importantly, it makes it easier to create utility functions which tell you if a transaction has reached a specific state. The description format follows [Xstate](https://xstate.js.org/docs/), which is a Finite State Machine (FSM) library. However, the library is not used since transitions in the actual state machine are handled by Marketplace API. ```diff filename="src/transactions/transactionProcessBooking.js" export const graph = { // id is defined only to support Xstate format. // However if you have multiple transaction processes defined, // it is best to keep them in sync with transaction process aliases. - id: 'default-booking/release-1', + id: 'booking-with-negotiation/release-1', // This 'initial' state is a starting point for new transaction initial: states.INITIAL, // States states: { [states.INITIAL]: { on: { [transitions.INQUIRE]: states.INQUIRY, - [transitions.REQUEST_PAYMENT]: states.PENDING_PAYMENT, + [transitions.REQUEST]: states.CUSTOMER_MADE_OFFER, }, }, [states.INQUIRY]: { on: { - [transitions.REQUEST_PAYMENT_AFTER_INQUIRY]: states.PENDING_PAYMENT, + [transitions.REQUEST_AFTER_INQUIRY]: states.CUSTOMER_MADE_OFFER, }, }, // etc. }, }; ``` When adding a new state, it needs to be added to the `states` property of `graph`. Transitions from one state to another are defined in the `on` property of a state. So, you need to add outbound transitions there and inbound transitions to the `on` property of the previous state(s). ### Update graph helper functions to match the new process Since the states and transitions in your state graph description have changed, you will need to review all the helper functions in your transaction process file and adjust them accordingly. For example, if you have different privileged transitions in your process than the ones in the default process, you will need to update the helper function to feature the correct transitions. You can identify privileged transitions in the transaction process graph by the lock icon next to the transition name. ```diff filename="src/transactions/transactionProcessBooking.js" export const isPrivileged = (transition) => { return [ - transitions.REQUEST_PAYMENT, - transitions.REQUEST_PAYMENT_AFTER_INQUIRY, + transitions.CUSTOMER_OFFER, + transitions.PROVIDER_OFFER, ].includes(transition); }; ``` ## Update state data for Inbox Page and Transaction Page In addition to the transaction process file, there are two other places where transaction process state data is handled: **InboxPage** and **TransactionPage**. Both of those containers have files you will need to review. Similarly to the `src/transactions` folder, you can either - modify the existing state data file whose states and transitions most closely match your new process, - or you can replicate the existing file into a new one and modify the new file In both InboxPage and TransactionPage contexts, the `...stateData.js` file compiles necessary transaction state information being used on the page. For instance, `TransactionPage` has an action button, and depending on the transaction state and the user's role in the transaction, the button may be used to accept or decline the transaction, or mark it as delivered or received. This logic is determined in the process specific `TransactionPage.stateData....js`. files. The following example is from [TransactionPage.stateDataPurchase.js](https://github.com/sharetribe/web-template/blob/main/src/containers/TransactionPage/TransactionPage.stateDataPurchase.js): ```jsx filename="TransactionPage.stateDataPurchase.js" // ConditionalResolver is basically a case structure // that takes the process state and transaction role as parameters return new ConditionalResolver([processState, transactionRole]) .cond([states.INQUIRY, CUSTOMER], () => { const transitionNames = Array.isArray(nextTransitions) ? nextTransitions.map((t) => t.attributes.name) : []; const requestAfterInquiry = transitions.REQUEST_PAYMENT_AFTER_INQUIRY; const hasCorrectNextTransition = transitionNames.includes( requestAfterInquiry ); const showOrderPanel = !isProviderBanned && hasCorrectNextTransition; return { processName, processState, showOrderPanel }; }) .cond([states.INQUIRY, PROVIDER], () => { return { processName, processState, showDetailCardHeadings: true }; }) .cond([states.PURCHASED, CUSTOMER], () => { // In the default purchase process, after the item has been purchased the customer // sees an action button to transition the transaction with 'mark-received-from-purchased' return { processName, processState, showDetailCardHeadings: true, showActionButtons: true, showExtraInfo: true, primaryButtonProps: actionButtonProps( transitions.MARK_RECEIVED_FROM_PURCHASED, CUSTOMER ), }; }) .cond([states.PURCHASED, PROVIDER], () => { // In the same state, the provider sees an action button to transition the transaction // with 'mark-delivered'. The component on the transaction page is the same, but the props are // resolved in this ...stateData... -file. const actionButtonTranslationId = isShippable ? `TransactionPage.${processName}.${PROVIDER}.transition-mark-delivered.actionButtonShipped` : `TransactionPage.${processName}.${PROVIDER}.transition-mark-delivered.actionButton`; return { processName, processState, showDetailCardHeadings: true, showActionButtons: true, primaryButtonProps: actionButtonProps( transitions.MARK_DELIVERED, PROVIDER, { actionButtonTranslationId, } ), }; }); /*...*/ ``` The main `...stateData.js` file imports functions from these process specific files, such as `...stateDataBooking.js` and `...stateDataPurchase.js`, to retrieve the state data corresponding to the correct process. ```jsx filename="TransactionPage.stateData.js" export const getStateData = (params) => { /* ... */ if (processName === PURCHASE_PROCESS_NAME) { return getStateDataForPurchaseProcess(params, processInfo()); } else if (processName === BOOKING_PROCESS_NAME) { return getStateDataForBookingProcess(params, processInfo()); } else if (processName === INQUIRY_PROCESS_NAME) { return getStateDataForInquiryProcess(params, processInfo()); } else if (processName === NEGOTIATION_PROCESS_NAME) { return getStateDataForNegotiationProcess(params, processInfo()); } else { return {}; } }; ``` If you have added a new process name constant besides the defaults listed in the above example, you will need to import it in the two `...stateData.js` files, as well as import the functions they need to use for retrieving state data, so that your Inbox Page and Transaction Page work correctly. The process specific `...stateData` files (e.g. `...stateDataBooking.js` and `...stateDataPurchase.js`) export a `getStateDataFor...` function, which conditionally resolves the necessary props based on the transaction state and the user role. You will need to check which changes to make in the `ConditionalResolver`, for example if there are new states that require specific props to be returned to the page based on the state. Depending on what props your user interface changes require, you may need to also pass custom props that are not used in the default processes. ```jsx filename="TransactionPage.stateDataNegotiatedBooking.js" export const getStateDataForNegotiatedBookingProcess = (txInfo, processInfo) => { const { transaction, transactionRole, nextTransitions } = txInfo; const isProviderBanned = transaction?.provider?.attributes?.banned; const isCustomerBanned = transaction?.provider?.attributes?.banned; const _ = CONDITIONAL_RESOLVER_WILDCARD; const { processName, processState, states, transitions, isCustomer, actionButtonProps, leaveReviewProps, } = processInfo; return new ConditionalResolver([processState, transactionRole]) .cond([states.INQUIRY, CUSTOMER], () => { const transitionNames = Array.isArray(nextTransitions) ? nextTransitions.map(t => t.attributes.name) : []; const requestAfterInquiry = transitions.REQUEST_AFTER_INQUIRY; const hasCorrectNextTransition = transitionNames.includes(requestAfterInquiry); const showOrderPanel = !isProviderBanned && hasCorrectNextTransition; return { processName, processState, showOrderPanel }; }) .cond([states.INQUIRY, PROVIDER], () => { return { processName, processState, showDetailCardHeadings: true }; }) .cond([states.CUSTOMER_MADE_OFFER, CUSTOMER], () => { return { processName, processState, showDetailCardHeadings: true, showExtraInfo: true }; }) .cond([states.CUSTOMER_MADE_OFFER, PROVIDER], () => { const primary = isCustomerBanned ? null : actionButtonProps(transitions.PROVIDER_ACCEPT, PROVIDER); const secondary = isCustomerBanned ? null : actionButtonProps(transitions.PROVIDER_DECLINE, PROVIDER); const tertiary = isCustomerBanned ? null : actionButtonProps(transitions.PROVIDER_OFFER, PROVIDER), return { processName, processState, showDetailCardHeadings: true, showActionButtons: true, primaryButtonProps: primary, secondaryButtonProps: secondary, tertiaryButtonProps: tertiary }; }) /* ... */ ``` If you have created a new **stateData** file (e.g. **TransactionPage.stateDataNegotiatedBooking.js**), you will need to export a uniquely named _getStateDataFor..._ function from that file and then use it in the `...stateData.js` file. ```diff filename="TransactionPage.stateData.js" export const getStateData = params => { /* ... */ if (processName === PURCHASE_PROCESS_NAME) { return getStateDataForPurchaseProcess(params, processInfo()); } else if (processName === BOOKING_PROCESS_NAME) { return getStateDataForBookingProcess(params, processInfo()); } else if (processName === INQUIRY_PROCESS_NAME) { return getStateDataForInquiryProcess(params, processInfo()); } else if (processName === NEGOTIATION_PROCESS_NAME) { return getStateDataForNegotiationProcess(params, processInfo()); + } else if (processName === NEGOTIATED_BOOKING_PROCESS_NAME) { + return getStateDataForNegotiatedBookingProcess(params, processInfo()); } else { return {}; } } ``` ## Update listing creation tabs In the EditListingWizard component, listing editing tabs are defined by transaction process. Add the tabs you want to use for listings using this process. ```diff filename="EditListingWizard.js" const tabsForListingType = (processName, listingTypeConfig) => { /* ... */ const tabs = { ['default-booking']: [DETAILS, ...locationMaybe, PRICING, AVAILABILITY, ...styleOrPhotosTab], + ['booking-with-negotiation']: [DETAILS, ...locationMaybe, PRICING, AVAILABILITY, ...styleOrPhotosTab], ['default-purchase']: [DETAILS, PRICING_AND_STOCK, ...deliveryMaybe, ...styleOrPhotosTab], ['default-negotiation']: [DETAILS, ...locationMaybe, ...pricingMaybe, ...styleOrPhotosTab], ['default-inquiry']: [DETAILS, ...locationMaybe, ...pricingMaybe, ...styleOrPhotosTab], }; return tabs[processName] || tabs['default-inquiry']; }; ``` ## Add marketplace text strings A lot of marketplace text strings in Sharetribe Web Template are transaction process and state specific. ```jsx filename="src/translations/en.json" /* ... */ "InboxPage.default-booking.accepted.status": "Accepted", "InboxPage.default-booking.canceled.status": "Canceled", "InboxPage.default-booking.declined.status": "Declined", "InboxPage.default-booking.delivered.status": "Delivered", /* ... */ ``` This means that when you create a new transaction process, you will also need to add marketplace texts for the relevant keys and states in the new process. You can add the process specific keys either into the [bundled marketplace text files in the template](/template/content-management/how-to-change-bundled-marketplace-texts/) or through the [Sharetribe Console > Build > Content > Marketplace texts section](https://www.sharetribe.com/help/en/articles/8404720-how-to-edit-marketplace-texts). After making the necessary changes in these contexts, your new transaction process should work as expected in the Template! Be sure to test all the steps in your transaction process carefully to make sure that all cases show up as you would expect. ## Add error handling on user deletion Because the Template offers support for users to delete their accounts, you will also need to ensure that newly created transaction processes (or modified ones) are properly checked at the time of deletion for states that contain Stripe-related payment processing. This is handled in [delete-account.js](https://github.com/sharetribe/web-template/blob/main/server/api/delete-account.js). If you have modified an existing transaction process, ensure that all states containing Stripe-related actions have been included in the appropriate array. See the default booking configuration for example: ```jsx filename="server/api/delete-account.js" const stripeRelatedStatesForBookings = [ 'state/pending-payment', 'state/preauthorized', 'state/accepted', 'state/delivered', ]; ``` Alternatively, if you have added an entirely new transaction process then you will need to create a new array and add it to the queue to be checked before submitting the user deletion request. ```diff filename="server/api/delete-account.js" + stripeRelatedStatesForNegotiatedBookings = [ + // ...add the relevant states + ]; + const ongoingNegotiatedBookingsWithIncompletePaymentProcessing = () => + sdk.transactions.query({ + processNames: 'booking-with-negotiation', + states: stripeRelatedStatesForNegotiatedBookings.join(','), + }); Promise.all([ /* ... */ + ongoingNegotiatedBookingsWithIncompletePaymentProcessing(), ]) /* ... */ }; ``` This handles throwing an error if the user attempts to delete their account while any of their involved transactions have ongoing Stripe payment processing. This also ensures that no expensive, unnecessary user deletion API calls are made, and that the user is prompted to contact the marketplace rather than receiving an unclear or confusing error. ## Summary Adding a new transaction process always requires changes in your client application. This article offers a starting point to adding your new process to the transaction process configuration and state handling. If your new process is very similar to one of the existing default processes, you may be able to implement by making the default changes to configuration and state handling. However, if your process is complex or it differs significantly from any of the existing processes, you will also need to carefully test the user experience and likely add new logic for the containers and components as well. --- ## Create a new transaction process with Sharetribe CLI Path: how-to/transaction-process/create-new-transaction-process-with-cli/index.mdx # Create a new transaction process with the Sharetribe CLI In this guide, we'll create a new transaction process for your marketplace. We will use the negotiated booking process, where the customer and provider first negotiate on the price of the booking before the customer makes the payment. Sharetribe CLI (Command-line interface) is a tool for changing your marketplace's configurations such as transaction processes and email templates. This guide expects that you have already installed Sharetribe CLI and are logged in with your API key. It's recommended to first read the tutorial [Getting started with Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/). If you haven't read [how transaction processes work in Sharetribe](/concepts/transactions/transaction-process/), it's a good idea to do that before starting this guide. If you want to edit an existing transaction process instead, you can follow these instructions to edit your transaction process: - [Edit a transaction process with Sharetribe CLI](/how-to/transaction-process/edit-transaction-process-with-sharetribe-cli/) ## Clone Sharetribe example processes repository When you are creating a new transaction process, you can either write your process from scratch, or you can use existing processes as your starting point. Since we are using one of Sharetribe's example processes in this guide, we will start by cloning the Sharetribe example processes repository: ```shell git clone https://github.com/sharetribe/example-processes.git ``` ## Create a new process To get up and running with Sharetribe CLI, see the [Getting started with Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/) guide in Dev Docs. Let's see what the subcommand `help` tells us about `process create`: ```shell $ flex-cli help process create create a new transaction process USAGE $ flex-cli process create OPTIONS --path=LOCAL_PROCESS_DIR path to the directory where the process.edn file is --process=PROCESS_NAME name for the new process that is created -m, --marketplace=MARKETPLACE_ID marketplace identifier ``` To create a new process, we need to specify a path to the local directory. That directory should contain two things: - the process definition (process.edn file) and - a `templates` subdirectory containing correct email templates for the email notifications defined in that process. We already have those since we cloned the _example-processes_ repository. If you are creating your own transaction process, make sure that you are including both the `process.edn` file and the `templates` directory in your process directory. In addition, we need to define a name for the process and the target marketplace environment. You can use the same process name, for example _"negotiated-booking"_, or you can name it something different, such as _"booking-with-negotiation"_. For clarity, in this example we'll use a different name for the new process. ```shell flex-cli process create --path=./negotiated-booking --process=booking-with-negotiation -m=my-marketplace-dev ``` After executing that command, you can go to the Sharetribe Console: _Build > Advanced > Transaction process visualizer_ and see that the _"booking-with-negotiation"_ process is there. ![Booking with negotiation process in Console.](./booking-with-negotiation-in-console.png) ## Create a process alias The process is created, but we still can't reference that process from our client app, since it doesn't have process alias set. We can create an alias for our new process with Sharetribe CLI command: ```shell flex-cli process create-alias --process=booking-with-negotiation --version=1 --alias=release-1 -m=my-marketplace-dev ``` With that command, we are creating a new alias _"release-1"_ and point it to the previously created process and its version 1. After that you should see the alias in the Console:
`booking-with-negotiation/release-1`. ![Process alias added to the process in Console.](./booking-with-negotiation-and-alias-in-console.png) Now, you can initiate transactions with this process using the API or SDK! ## Summary In this guide, you added a new transaction process in your Sharetribe marketplace environment, and set an alias to the process so you can reference it with the API. If you are using the Sharetribe Web Template, you also need to modify your client app to use this process. You can follow the steps in this guide to do that: - [Change transaction process in Sharetribe Web Template](/how-to/transaction-process/change-transaction-process-in-template/) --- ## Edit an existing transaction process with Sharetribe CLI Path: how-to/transaction-process/edit-transaction-process-with-sharetribe-cli/index.mdx # Edit a transaction process with Sharetribe CLI In this guide, we will extend the marketplace review period with the Sharetribe CLI. After we've made the change, we'll push the updated transaction process version and update the existing alias. Sharetribe CLI (Command-line interface) is a tool for changing your marketplace's configurations such as transaction processes and email templates. This guide expects that you have already installed Sharetribe CLI and are logged in with your API key. It's recommended to first read the tutorial [Getting started with Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/). If you haven't read [how transaction processes work in Sharetribe](/concepts/transactions/transaction-process/), it's a good idea to do that before starting this guide. If you want to create a new transaction process based on an existing one instead, you can follow these instructions to create a new transaction process and alias: - [Create a new transaction process with the Sharetribe CLI](/how-to/transaction-process/create-new-transaction-process-with-cli/) Let's get started! ## Pull the existing process The first thing to do is to list all the existing processes with CLI command `process list`. Remember to include your marketplace ident to the command with the `--marketplace ` options or the short version `-m `: ```bash flex-cli process list -m my-marketplace-dev ``` From the list of processes, pick the one that you want to edit. In this guide, we'll use process `default-booking`, version 1. You might have different processes in your marketplace, so you can use those. However, you can check the content of this default booking example process online from [Sharetribe example processes](https://github.com/sharetribe/example-processes) Github repository. We can pull the process with the `process pull` command. Let's see the options that the command needs: ```bash flex-cli help process pull ``` We can see that required options are: - `--path` the path where the process is saved - `--process` name of the process - `--version` or `--alias`. Process version or alias pointing to the version that we want to pull - `--marketplace` the marketplace Pull the process and save it to `process` directory: ```bash flex-cli process pull --process default-booking --version 1 --path process -m my-marketplace-dev ``` See what's inside the `process` directory: ```bash ls process ``` (Windows users: use `dir` instead of `ls`) You can see that there are two items in the directory: - `process.edn` file, which defines the transaction process - `templates` directory, which contains all the transaction email templates for this process Next, we're going to edit the process description. ## Extend the review period Open the `process.edn` in your favorite editor. _(To get proper syntax highlighting, you may need to install a plugin to your editor. edn is subset of Clojure, so a Clojure plugin will give you proper edn highlighting.)_ From the `process.edn` file, you'll find a map with a key `:transitions`. The value of the `:transitions` key is a vector of transition in your marketplace. Each transition contains values for keys like `:name`, `:actor`, `:actions`, `:to` and `:from`. To extend the review period in the transaction process, we need to find the transition where the review periods are defined. In the Sharetribe default processes, the review period length is defined in the transition `:transition/expire-review-period`, and if one participant has already reviewed the other, in either `:transition/expire-provider-review-period` or `:transition/expire-customer-review-period`. By default, the review periods are defined as 7 days. When you have found the transitions, change the value of the `:fn/period` in the `:at` time expression to `["P10D"]` to extend the review period to 10 days. ```diff {:format :v3 :transitions [{:name :transition/inquire, ;;... {:name :transition/expire-review-period, :at {:fn/plus - [{:fn/timepoint [:time/booking-end]} {:fn/period ["P7D"]}]}, + [{:fn/timepoint [:time/booking-end]} {:fn/period ["P10D"]}]}, :actions [], :from :state/delivered, :to :state/reviewed} {:name :transition/expire-provider-review-period, :at {:fn/plus - [{:fn/timepoint [:time/booking-end]} {:fn/period ["P7D"]}]}, + [{:fn/timepoint [:time/booking-end]} {:fn/period ["P10D"]}]}, :actions [{:name :action/publish-reviews}], :from :state/reviewed-by-customer, :to :state/reviewed} {:name :transition/expire-customer-review-period, :at {:fn/plus - [{:fn/timepoint [:time/booking-end]} {:fn/period ["P7D"]}]}, + [{:fn/timepoint [:time/booking-end]} {:fn/period ["P10D"]}]}, :actions [{:name :action/publish-reviews}], :from :state/reviewed-by-provider, :to :state/reviewed} ``` Save the changes you've made to `process.edn` file. ## Validate and push the process After each modification to the `process.edn` file, it's good idea to validate that the changes are correct. The CLI command `process` can do this: ```bash flex-cli process --path process ``` In case the `process.edn` file is valid, you'll see a description of your process with all the states, transitions and notifications listed. In case the file is invalid, you'll see a validation error. Make sure you are using the latest CLI version when validating the process to avoid unnecessary errors. {/* prettier-ignore */} ```shell yarn global add flex-cli ``` ```shell npm install --global flex-cli ``` {/* // prettier-ignore */} You can see the CLI version you are using by running `flex-cli version`. Now that we have validated the `process.edn` file we are ready to push the changes to Sharetribe: ```bash flex-cli process push --path process --process default-booking -m my-marketplace-dev ``` After the process is successfully pushed, you'll see a new process version in [Console](https://console.sharetribe.com/advanced/transaction-processes). Please note that pushing the changes to Sharetribe doesn't immediately change the way how your marketplace works. The existing transactions are still using the old process version and the new transaction will be using the old process as long as the existing alias is pointing to it. So, to take the changes into use, let's update the alias! ## Update alias First, let's see what aliases are pointing to which versions. We can do this by using the `process list` command with the `--process` option: ```bash flex-cli process list --process default-booking -m my-marketplace-dev ``` You'll see a list of process versions and aliases pointing to them. The alias always consists of two parts where the first part is the process name and the second part is the alias name. You'll also see that the newly created version doesn't have an alias pointing to it. Let's change that. In the default process, the name of the existing alias is `release-1`. The command to update the alias is `process update-alias`: ```bash flex-cli process update-alias --process default-booking --alias release-1 --version 2 -m my-marketplace-dev ``` This command updates the alias `release-1` to point to `default-booking` process version `2`. To verify that the change was successful, you can rerun the `process list` command and see that the `release-1` alias is now pointing to the version 2. **Be careful when updating aliases!** Updating the alias will take the process changes in use immediately. In case you make changes where you add/remove/rename states or transitions, updating alias may potentially break your marketplace front-end if you haven't updated it to work with the new process. The review period has now been changed! Next time you initiate a new transaction with the alias `default-booking/release-1` the review period is 10 days. ## Summary In this tutorial we pulled an existing process definition with the Sharetribe CLI. We extended the review period to 10 days, validated the process file and pushed it back to Sharetribe. Finally, we updated the alias to point to the new version. You know now how to make simple modifications to the process. Have a look at the [transaction process format](/references/transaction-process-format/) reference and the [transaction process actions](/references/transaction-process-actions/) reference to read about all the possibilities that transaction process engine gives to you. When you modify the transitions or states in your transaction process when editing your process, you will need to update your client application accordingly. If you are using the Sharetribe Web Template, you can follow these instructions to reflect your changes in the client: - [Change transaction process in Sharetribe Web Template](/how-to/transaction-process/change-transaction-process-in-template/) You may also want to edit the transaction email templates. You can follow these instructions to modify your email templates: - [Editing email templates with Sharetribe CLI](/how-to/emails-and-notifications/edit-email-templates-with-sharetribe-cli/) --- ## Enable Facebook login Path: how-to/users-and-authentication/enable-facebook-login/index.mdx # Enable Facebook login Enabling Facebook login consists of three main steps: - **Create a Facebook app** Facebook app is what connects your marketplace to Facebook and lets Facebook know that users from your marketplace are allowed to authenticate themselves using the Facebook login. - **Create an identity provider client in Sharetribe Console** Identity provider (IdP) client is what let's Sharetribe know that the users of your marketplace are allowed to use the Facebook app you created to log into your marketplace. - **Add environment variables to the Sharetribe Web Template** A few attributes from the Facebook app will need to be configured to your Sharetribe Web Template so that it can perform the login flow via Facebook. Facebook Login can be configured without code if you are running a no-code marketplace. If you are hosting the template yourself, you will need to follow the instructions on this page. Compared to no-code marketplaces, the key difference is that you **need to configure the Client Secret using environment variables** instead of configuring the value via Console. ## Create a Facebook app The first thing to do is to create a Facebook development app for your marketplace by following [these steps in the Sharetribe help center](https://www.sharetribe.com/help/en/articles/9174337-how-to-enable-facebook-login#h_3848596b4d). ## Configure an identity provider client in Console Now that your Facebook app is all set up, a corresponding _identity provider client_ will need to be configured for your marketplace. This will tell Sharetribe that your users will be allowed to log into your marketplace using the Facebook app you just created. The information stored in an IdP client is used to verify a token obtained from Facebook when a user logs in. An identity provider client can be configure with the following steps: {

} Go to [Social logins & SSO in Console](https://console.sharetribe.com/advanced/social-logins-and-sso). {

} Under _Identity provider clients_ click "+ Add new". {

} Set "Client name". This can be anything you choose, for example, "Facebook login". In case you need to create multiple Facebook apps, this will help you make a distinction between the corresponding IdP clients. {

} Set the _Client ID_. This value is the App ID from your Facebook app. You can see the value under _Settings > Basic_ in the Facebook app view. {

} Set the _Client secret_. This value is the App secret in your Facebook app. You can see the value under _Settings > Basic_ in the Facebook app view. You will need to authenticate to reveal the secret value. You will also need to configure the Client secret and the Client ID as environment variables. The IdP client configuration should now look something like this: ![Add identity provider client](./add-idp-client.png) {

} Click "Add client" and your identity provider client is ready. ## Add environment variables Last step to enabling Facebook login is to configure your Sharetribe Web Template with the values that you used to add an identity provider client in Console. Add the following environment variables to your template: - **`REACT_APP_FACEBOOK_APP_ID`** The App ID of your Facebook app. You can see the value under Settings > Basic in the Facebook app view. Also corresponds to _Client ID_ value of the identity provider in Console. - **`FACEBOOK_APP_SECRET`** The App Secret of your Facebook app. Also corresponds to _Client secret_ value of the identity provider in Console. Remember to redeploy your app or restart your development environment after making changes to environment variables! For more information on the template environment variables, see the [Template environment variables](/template/configuration/template-env/) article. That is it. Setting these environment variables will make Sharetribe Web Template render the Facebook login button in signup and login forms. --- ## Enable Google login Path: how-to/users-and-authentication/enable-google-login/index.mdx # Enable Google login Enabling Google login consists of three main steps: - **Create a Google Sign-In Project** Google Sign-In Project is what connects your marketplace to Google and let's Google know that users from your marketplace are allowed to authenticate themselves using the Google Sign-In. - **Create an identity provider client in Sharetribe Console** Identity provider (IdP) client is what let's Sharetribe know that the users of your marketplace are allowed to use the Google Sign-In Project you created to log into your marketplace. - **Add environment variables to the Sharetribe Web Template** A few attributes from the Sign-In Project will need to be configured to Sharetribe Web Template so that it can perform the login flow via Google. Google Login can be configured without code if you are running a no-code marketplace. If you are hosting the template yourself, you will need to follow the instructions on this page. Compared to no-code marketplaces, the key difference is that you **need to configure the Client Secret using environment variables** instead of configuring the value via Console. ## Create a Google Sign-In Project The first thing to do is to create a Google Sign-In project for your marketplace by following [these steps in the Sharetribe help center](https://www.sharetribe.com/help/en/articles/9174430-how-to-enable-google-login#h_1e6314ef08). ## Configure an identity provider client in Console Now that your Google Sign-In project is all set up, you will need to configure a corresponding _identity provider client_ for your marketplace. This will tell Sharetribe that your users will be allowed to log into your marketplace using the Google Sign-In you just created. The information stored in an IdP client is used to verify a token obtained from Google when a user logs in. An identity provider client can be configure with the following steps: {

} Go to [Social logins & SSO in Console](https://console.sharetribe.com/advanced/social-logins-and-sso). {

} Under _Identity provider clients_ click "+ Add new". {

} Set "Client name". This can be anything you choose, for example, "Google login". {

} Under "Select identity provider", pick "Google". {

} Set the _Client ID_. This value is the Client ID from your Google Sign-In project. You can see the value under _Credentials > OAuth 2.0 client IDs_. Make sure you have the project you just created selected from the top bar in Google developer console. In case you have multiple clients configured in Google Sign-In, use the client ID of your _Web application_ client here. See the next step for more information. The IdP client configiruation should now look something like this: ![Add Google identity provider client](./add-google-idp-client.png) {

} If you have more than one client configured in your Google Sign-In project, mobile clients for example, add the additional client IDs to the same identity provider client under "Trusted client IDs" by clicking "+ Add new trusted client". In case you are using two distinct Google Sign-In projects, configure those as distinct clients in Console but always bundle all the client IDs of a single project into one identity provider client in Sharetribe Console. {

} Click "Add client" and your identity provider client is ready. ## Add environment variables Last step to enabling Google login is to configure your Sharetribe Web Template with the values that you used to add an identity provider client in Console. Add the following environment variables to the template: - **`REACT_APP_GOOGLE_CLIENT_ID`** The Client ID of your Google Sign-In. You can see the value under _Credentials > OAuth 2.0 client IDs_ in your Google Sign-In project. Also corresponds to _Client ID_ value of the Identity provider client you created in Console. - **`GOOGLE_CLIENT_SECRET`** The Client Secret of your Google Sign-In. Remember to redeploy your app or restart your development environment after making changes to environment variables! For more information on the template environment variables, see the [Template environment variables](/template/configuration/template-env/) article. That is it. Setting these environment variables will make Sharetribe Web Template render the Google login button in signup and login forms. --- ## Enable OpenID Connect login Path: how-to/users-and-authentication/enable-open-id-connect-login/index.mdx # Enable OpenID Connect login ## OpenID Connect [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) is a specification built on OAuth2 that describes how a user authenticated at an identity provider can be authorized to resources in another service. This how-to guide assumes that you already have an OpenID Connect solution available and intend to use that as a login option in your Sharetribe marketplace. ## Identity provider requirements ### Discovery document and JSON Web keys All identity providers should provide an [OpenID Connect discovery document](https://openid.net/specs/openid-connect-discovery-1_0.html). The document has to define a `jwks_uri` attribute which denotes the location of public signing keys used by the identity provider. The signing keys should be served in the `jwks_uri` location in [JSON Web Key Set format](https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41). ### Signing algorithms Sharetribe only supports ID tokens signed with asymmetric RS256 signing algorithm. The identity provider should provide public signing keys as mentioned above. ### Rotating signing keys Sharetribe relies heavily on the `kid` attribute of a JSON Web Key when caching signing keys. We advise that every OpenID Connect identity provider includes the `kid` attribute in signing keys and in ID token header. Especially, when signing keys are rotated, it is critical to have the `kid` attribute in JWKs and a corresponding `kid` header in the ID token. ## Configure an identity provider client in Console To take an OpenID Connect identity provider into use with Sharetribe, you will need to configure a new identity provider and an accompanying identity provider client in Sharetribe Console. {

Add a new IdP Client

} Go to the [Social logins & SSO](https://console.sharetribe.com/advanced/social-logins-and-sso) page in Console and click "+ Add new" to add a new identity provider client. Fill in a name for the client. {

Add a new provider

} In the identity provider dropdown, select "+ Add a new identity provider..." ![Add OpenID Connect client](./oidc-client-1.png) {

Fill in information

} Fill in information regarding your OpenID Connect identity provider. This is the service that your users authenticate to in order to log into Sharetribe. - **Identity provider name**: A descriptive name for the identity provider that helps you to distinguish it from other providers. - **Identity provider ID**: IdP ID that is passed as a parameter to Sharetribe API when authenticating using this client/IdP. It is generated based on the provider name - **Identity provider URL**: In OpenID Connect terms this is the _issuer location_ of the identity provider. It is used to resolve ID token signing keys used by the identity provider. See below _Discovery document and JSON Web keys_ for more details. {

Add Client ID

} Fill in the Client ID. This is the identifier of your Sharetribe marketplace at you identity provider. It will be the _audience_ of the ID token returned from the identity provider. {

List additional clients

} If you have multiple clients configured at your identity provider to be used to log into your Sharetribe marketplace, list the additional client IDs as trusted client IDs. The idea is, that every client ID that is included as an audience (`aud` claim) in an ID token returned from your identity provider should be included as the client ID or trusted client ID in the client. ![Add OpenID Connect client](./oidc-client-2.png) {

Add client

} Click "Add client" to create the client and identity provider. Now that you have created the identity provider, you can use it if your login flow requires using another client or if you wish to remove the client you added and create a new one. Just select the identity provider from the dropdown when creating a new client. ## Add OpenID Connect login flow to Sharetribe Web Template OpenID Connect login flow can be added to Sharetribe Web Template in multiple ways. One good starting point is to take a look at OpenID Connect implementations in [the Passport.js strategies](http://www.passportjs.org). We also recommend using the default [facebook.js](https://github.com/sharetribe/web-template/blob/main/server/api/auth/facebook.js) and [google.js](https://github.com/sharetribe/web-template/blob/main/server/api/auth/google.js) files as a basis for your modifications. On a high level, you can follow these steps to get started with your integration. {

Create auth file

} Replicate either [facebook.js](https://github.com/sharetribe/web-template/blob/main/server/api/auth/facebook.js) or [google.js](https://github.com/sharetribe/web-template/blob/main/server/api/auth/google.js) and rename it for your identity provider. {

Update passport strategy

} Replace the [passport module](https://github.com/sharetribe/web-template/blob/main/server/api/auth/facebook.js#L2) with one for your provider. Update the [strategy options](https://github.com/sharetribe/web-template/blob/main/server/api/auth/facebook.js#L22-L28) accordingly. {

Configure credentials

} Add your provider and Sharetribe SSO client credentials to `.env`. Update [your new file](https://github.com/sharetribe/web-template/blob/main/server/api/auth/facebook.js#L8-L9) and [createWithIdp.js](https://github.com/sharetribe/web-template/blob/main/server/api/auth/createUserWithIdp.js#L12-L16). Also update the [identity provider selection logic](https://github.com/sharetribe/web-template/blob/main/server/api/auth/createUserWithIdp.js#L49-L50). {

Define a callback

} Import your new file in [server/apiRouter.js](https://github.com/sharetribe/web-template/blob/main/server/apiRouter.js#L21) and define [callback endpoints](https://github.com/sharetribe/web-template/blob/main/server/apiRouter.js#L65-L71). Add the callback URL to your provider’s settings. {

Add login buttons

} Add buttons for your service in the AuthenticationPage [SocialLoginButtons component](https://github.com/sharetribe/web-template/blob/main/src/containers/AuthenticationPage/SocialLoginButtons/SocialLoginButtons.js). {

Implement verify callback

} Update the [verifyCallback function](https://github.com/sharetribe/web-template/blob/main/server/api/auth/facebook.js#L45-L61) to parse provider response parameters, extract the ID token, and pass it to `done`. This ensures tokens flow correctly to Sharetribe’s [`/auth/auth_with_idp`](https://www.sharetribe.com/api-reference/authentication.html#issuing-tokens-with-an-identity-provider) and [`current_user/create_with_idp`](https://www.sharetribe.com/api-reference/marketplace.html#create-user-with-an-identity-provider) endpoints. ## Identity provider specific information ### Apple Sign-in Apple Sign-in has several features that resemble the OpenID Connect specification. However, it is not explicitly stated to be OpenID Connect compliant. To the knowledge of our team, the [Apple Sign-in token](https://developer.apple.com/documentation/signinwithapple/authenticating-users-with-sign-in-with-apple) should be compliant with the Sharetribe back-end at the time of this writing (2024-05), but you will need to conduct your own testing to verify this. Apple Sign-in also requires developers to set up a [private email relay service](https://developer.apple.com/documentation/signinwithapple/communicating-using-the-private-email-relay-service/) for Apple users who do not want to share their email address with the service. If you do integrate Apple Sign-in, it is important that you provide Sharetribe with a valid email address even if the user requests to keep their own address private. At the moment, the Sharetribe managed Sendgrid setup cannot handle private relay addresses, so at the very least you will need to [use your own Sendgrid account](/how-to/emails-and-notifications/set-up-outgoing-email-settings/#using-your-own-sendgrid-account). ### Auth0 Auth0 requires identity provider URL with a trailing slash, but Sharetribe Console does not currently allow adding trailing slashes. If you are adding an Auth0 integration, add the URL without the trailing slash, and reach out to Sharetribe Support so we can manually fix the formatting. ### LinkedIn Sign In with LinkedIn was updated to support Open ID Connect in August 2023. The corresponding Passport strategy has been updated to version 3.0.0 support Open ID Connect in the [Github repository](https://github.com/auth0/passport-linkedin-oauth2). However, at the time of this writing, only version 2.0.0 seems to be available through _npm_ and _yarn_ package managers. This means that to use version 3.0.0 in your integration, you will need to install the package directly from the Github repository: ```shell ## yarn add # yarn add https://github.com/auth0/passport-linkedin-oauth2#v3.0.0 ``` --- ## Extend user data in Sharetribe Web Template Path: how-to/users-and-authentication/extend-user-data-in-template/index.mdx # Extend user data in Sharetribe Web Template This guide shows you how to expand the user data model in your marketplace with code. We'll have a look on how the user can be configured so that the data gets added, and how it can then be presented. Adding new attributes to the data model relies on [extended data](/references/extended-data/). In Sharetribe Web Template, starting from release [v5.0.0](https://github.com/sharetribe/web-template/releases/tag/v5.0.0), top-level user extended data can be configured in the [configUser.js](https://github.com/sharetribe/web-template/blob/main/src/config/configUser.js) file. As of November 2025, public and private user fields can be configured in Console. Protected data user fields still need to be configured in configUser.js. In addition, Console allows configuring up to 100 user fields. If you have more than 100 user fields configured in Console, for example if you have a complex user type and user field setup on your marketplace, you will need to configure the rest of your user fields in configUser.js. Settings configured in local configurations files are overridden by any fetched via the Asset Delivery API. You can refer to [this article](/template/configuration/hosted-and-local-configurations/) to modify the way your template merges local and hosted configurations. Configuring the user data this way allows you to - declare the attribute and its possible values - show the attribute selection inputs in the signup page, and - optionally show public attribute values on the user's profile page ## Add a new top-level protected data attribute Let's extend the default user data by adding a protected data attribute 'arrivalInstructions' to allow providers to share how customers can get to their service facility. Adding the attribute as protected data allows us to expose the attribute to the other party of the transaction – [see these instructions](/tutorial/use-protected-data-in-emails/) for more details. The full configuration looks like this: ```js { key: 'arrivalInstructions', scope: 'protected', schemaType: 'text', showConfig: { label: 'How do people arrive at your facility?', }, saveConfig: { label: 'How do people arrive at your facility?', displayInSignUp: true, isRequired: true, }, // If you have defined user types, you can limit // individual user fields to specific user types: userTypeConfig: { limitToUserTypeIds: true, userTypeIds: ['provider'], }, }, ``` ### Declare the attribute and its possible values Extended data attributes in the `configUser.js` file need to be defined, at minimum, by **key**, by **scope**, and by **schemaType**. ```js key: 'arrivalInstructions', scope: 'protected', schemaType: 'text', ``` This attribute is defined as **protected**, so it will be saved into the user's profile as `protectedData.arrivalInstructions`. The **schemaType** attribute determines the shape of the data being saved: - **enum** attributes are saved as a single string value from a list of predefined options - **multi-enum** attributes are saved as an array of string values from a list of predefined options - **boolean** attributes are saved as **true** or **false** boolean values - **long** attributes are saved as long i.e. as an 8-byte whole number - **shortText** attributes are saved as a single short-text entry e.g. a URL - **text** attributes are saved as a single text entry If the schema type is **enum** or **multi-enum**, you will need to define an array of `enumOptions` for the attribute. This allows the user profile page and signup page to show the options when your user creates or edits their profile. ### Configure the signup page The `ProfileSettingsPage` and `AuthenticationPage` are configured to show specific inputs for specific schema types. This means that you only need to configure how the attribute shows up in the panel. By default, public user fields are collected on the profile settings page and shown on the public user profile page. All configured user fields are collected on the authentication page. If you want to hide a field from the signup page, you will need to set `displayInSignup` as `false`. Starting in template version [10.3.0](https://github.com/sharetribe/web-template/releases/tag/v10.3.0), private and protected data fields are modified in `ManageAccountPage`. In earlier versions, all user fields were shown and modified on `ProfileSettingsPage`. If you have added user fields with non-public scopes (private and protected) in an earlier version, double check whether you want to keep editing those fields on the `ProfileSettingsPage` or move the logic onto the `ManageAccountPage`. You can separately determine the label for editing the attribute and displaying the attribute, if it is displayed publicly. You can also set the attribute as required. ```js saveConfig: { label: 'How do people arrive at your facility?', displayInSignUp: true, isRequired: true, }, ``` ### Configure the profile page for public fields Protected and private user fields are managed on the ManageAccountPage, and they are not displayed on other pages. For these scopes, in other words, there is no display configuration. For public user fields, the configuration for showing top-level extended data on the profile page is straightforward. By default, all public user config attributes with a `showConfig.label` are shown on the user's public profile page, but by setting `displayInProfile` to `false` on an attribute with schema type `enum`, `long`, or `boolean`, you can hide the attribute from the Details section on the profile page. ```js // showConfig: { // label: 'Do you offer other services besides bike rentals?', // displayInProfile: true, }, ``` And that is it! With this configuration, a custom user attribute can be added to the user's extended data on authentication page, and public fields can be shown on the user's profile page. --- ## How to set up OpenID Connect proxy in Sharetribe Web Template Path: how-to/users-and-authentication/setup-open-id-connect-proxy/index.mdx # How to set up OpenID Connect proxy in Sharetribe Web Template The OpenID Connect (OIDC) support in Sharetribe allows you to integrate login solutions that do not necessarily implement OpenID Connect. The idea is to build a suitable login flow in Sharetribe Web Template and wrap that login information into an OpenID Connect ID token that can be used to validate user login in Sharetribe. With this approach, the template will serve as an identity provider towards Sharetribe. Sharetribe verifies the ID token by - fetching the JSON Web Key that is hosted by your template server, and - using that to unsign the token. A consequence of this is that the JSON Web Key needs to be publicly available. This means that the proxy setup will not work directly in localhost. To test out the Github login, you should e.g. [deploy your template changes to Render](/tutorial/deploy-to-render/#deploy-to-render). In this guide, we'll integrate Github login to Sharetribe by using Sharetribe Web Template as an OIDC proxy to Sharetribe. The main steps to take to achieve this are: {

} Create an OAuth app in Github {

} Configure a new identity provider and client in Sharetribe Console {

} Build Github auth flow in the template ## A note about development environments For OpenID Connect (OIDC) identity providers, Sharetribe supports RSA signed ID tokens. RSA is an asymmetric signing function. Therefore, all OIDC identity providers will need to provide their URL (also known as _issuer location_) to Sharetribe so that public signing keys can be fetched for ID token validation. When using Sharetribe Web Template as an OIDC proxy, it should be served publicly, so that Sharetribe can fetch the public signing key used to sign ID tokens used with authentication. This means that when developing OIDC proxy capabilities, by default, a template application running in `localhost` can not be used as an OIDC proxy but the application should be deployed, for example, to a staging environment. If you want to develop this functionality complete locally, take a look at tools like [Ngrok](https://ngrok.com/) or [Localtunnel](https://localtunnel.github.io/www/) that allow exposing your local ports publicly. When serving the template via Ngrok, you'll want to make sure the `issuerUrl` matches the URL generated by Ngrok where your site is accessible. This might default to localhost if your `NODE_ENV` variable is set to _development_. If you're using the `yarn run dev-server` command to run the template, this command overwrites the `REACT_APP_MARKETPLACE_ROOT_URL` specified in the .env file. Ensure you update the command to match the URL where your site is being served through Ngrok. ## Create an OAuth app in Github ### Head to Github developer settings Head to [Github developer settings](https://github.com/settings/developers). A Github account is required. ### Register a new application Select "OAuth apps" in the left sidebar, and click "Register a new application". ### Add application details Add application name and homepage URL. The name and URL are used to identify your application to the people signing in. ![Authentication view for Github](./github-proxy-auth-screen.png) ### Add Authorization callback URL Add the Authorization callback URL: `/api/auth/github/callback`. So for example, `https://www.mymarketplace.com/api/auth/github/callback` ### Register the application Click "Register application". ### Generate client secret Generate a client secret, and make a note of the client ID and client secret. You will need these values later on. ## Configure a new identity provider and client in Sharetribe Console With this proxy implementation, **your Sharetribe Web Template works as the identity provider towards Sharetribe.** Sharetribe uses your template application to validate the ID token that wraps the Github login information. To enable logins in Sharetribe using the OIDC proxy, a corresponding identity provider and identity provider client need to be configured for your marketplace in Sharetribe Console. See the [OpenID Connect how-to guide](/how-to/users-and-authentication/enable-open-id-connect-login/#configure-an-identity-provider-client-in-console) for information on how to add a new identity provider for your marketplace. Here's some guidance for configuring your template application as a new identity provider and a client to be used as a proxy for Github. ### Identity provider name and ID The identity provider ID is generated based on the name of the IdP. The ID will be passed to the Sharetribe API when creating a user or logging in using the proxy. When a user logs in with an identity provider, their identity provider profile is linked to their user account and this relationship is exposed in the [currentUser resource](https://www.sharetribe.com/api-reference/marketplace.html#currentuser-identity-provider) in the Sharetribe API. If the intention is to use the Sharetribe Web Template to proxy login to multiple services, it's advised to create a distinct identity provider for each, and name them so that the ID indicates what is the actual service providing the authentication. In Github's case the IdP name could be "Template Github" or "Template Github Proxy". ### Identity provider URL Based on this URL, Sharetribe determines the path to an OpenID Connect discovery document (_[identity provider URL]/.well-known/openid-configuration_) and from there on to an ID token signing key. In Open ID Connect terms, this is the issuer URL. In this setup, your Sharetribe Web Template acts as the issuer towards Sharetribe, so the URL should point to your template. By default, the identity provider URL should be the root address of your template application, for example, `https://example.com` or, for default Render URLs, `https://EXAMPLE.onrender.com`. Note, that this URL needs to be publicly hosted so a `localhost` URL will not work. ### Client ID When using Sharetribe Web Template as on OpenID Connect proxy, you are in charge of generating a client ID. The value can be any randomly generated string. ## Build Github auth flow in Sharetribe Web Template ### Sharetribe Web Template as an OpenID Connect identity provider The Sharetribe Web Template provides a few helper functions which you can use as a starting point in your customization. When following this guide, you will not need to pay too much attention to them as the crucial code is provided for you in the `github.js` file below, but it's good to be aware of them. You can find these functions in the `api-util/idToken.js` file in your server: **`createIdToken`** Turns information fetched from a 3rd party identity provider (e.g. Github) info a signed JSON Web Token (JWT). This function expects three parameters: _idpClientId_, _user_ and _options_. - _idpClientId_ is the client id of your custom identity provider you have set up in Console: - _user_ object should contain at least _firstName_, _lastName_, _email_ and _emailVerified_ fields. If these fields are not provided in the identity provider token, the user will need to enter them manually. - _options_ object contains information about how the id token should be signed and the keys required for that. Currently, Sharetribe supports only RS256 signing algorithm so the _options_ object should look like this: ``` { signingAlg: 'RS256', rsaPrivateKey, keyId } ``` **`openIdConfiguration`** and **`jwksUri`** These functions can be used to serve an OpenID Connect discovery document and JSON Web Keys that are used by Sharetribe to validate the ID token written by your proxy implementation. Sharetribe Web Template will automatically use these functions to expose correct endpoints when JWT signing keys are configured. ### Generate an RSA key pair A RSA public and private key pair is used to sign and validate an ID token that is passed from the template application to Sharetribe during the login/signup flow. When a user successfully logs into Github, the template wraps the user information to an ID token that is signed with a private key. The corresponding public key is served by the template in `/.well-known/jwks.json` and it is fetched by Sharetribe when an ID token is validated. In order for the Sharetribe Web Template to operate as an OpenID Connect identity provider, you will need to generate a RSA key pair. Both keys need to be in PEM format. The keys can be generated with `ssh-keygen` command line tool by running the following commands. The first one will generate a key pair, with the private key in PEM format and the public key in SSH public key format. The second command will create a public key in PEM format based on the public key file from the first command. ```bash # create an RSA key pair, you can leave out the passphrase when prompted ssh-keygen -f swt_rsa -t rsa -m PEM # now you have two files # swt_rsa: private key in PEM format # swt_rsa.pub: public key in SSH public key format # convert the public key from previous command to PEM format ssh-keygen -f swt_rsa.pub -e -m PEM > swt_rsa_pub ``` Now you have two files: `swt_rsa` and `swt_rsa_pub` (also you have `swt_rsa.pub` but that one you don't need). The content of the files should look like the following: ```txt # swt_rsa -----BEGIN RSA PRIVATE KEY----- private key value here -----END RSA PRIVATE KEY----- # swt_rsa_pub -----BEGIN RSA PUBLIC KEY----- public key value here -----END RSA PUBLIC KEY----- ``` We will use these key values to configure your application in the next section. ### Configure Sharetribe Web Template Add the following environment variables: `REACT_APP_GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` Set these as the client ID and client secret of your Github app. `RSA_PRIVATE_KEY` and `RSA_PUBLIC_KEY` The RSA key pair we created in the previous section The keys are multi-line strings but Heroku is fine with that so you can paste the keys in config vars as they are. Make sure to include the `-----BEGIN ... KEY-----` and `-----END ... KEY-----` lines in your environment variables, as they are a part of the key. If you are using Render or some other environment that requires you to declare environment variables through a file, wrap the RSA keys with quotation marks `"` and escape line breaks with the newline character `\n`. Make sure that the RSA key is defined on a single line. `GITHUB_PROXY_IDP_ID` The identifier of your identity provider that you configure to Sharetribe. It declares that you are using your template OpenID Connect proxy as an identity provider. Use the "IdP ID" value of an identity provider client in Console for this variable. `GITHUB_PROXY_CLIENT_ID` The client ID of your identity provider client that you configure to Sharetribe. Use the "Client ID" value of an identity provider client in Console for this variable. `KEY_ID` The value will be used as the `kid` header in ID tokens that are passed to Sharetribe when a user logs in with Github. It is also used as the `kid` attribute of the JSON Web key that the proxy serves in an endpoint. Even though using a _kid_ value in your keys is not compulsory, we heavily recommend using it with your token and the JWK. For example, key caching in the Sharetribe API relies heavily on it. This value can be a random string. ### Add Passport module dependency We are using [Passport.js](http://www.passportjs.org) library for handling the authentication with different identity providers like with Facebook and Google. The library offers multiple authentication strategies and there's also [a strategy for Github](https://www.passportjs.org/packages/passport-github2/), which we are going to use in this example. Run the following command in your terminal to install the package: {/* prettier-ignore */} ```shell yarn add passport-github2 ``` ```shell npm install passport-github2 ``` {/* // prettier-ignore */} ### Implement the Github login flow in Sharetribe Web Template backend Next, let's add a new file to the template that handles authentication to Github. You can find the complete file here: - [github.js](https://www.sharetribe.com/docs/tutorial-assets/github.js) Place the file in the `server/api/auth` folder: The biggest difference between Github login and e.g. Facebook login, which has first-class support in Sharetribe, is that we need to use a _createIdToken_ helper function in _verifyCallback_ to create the id token from the information we fetched from Github. This new id token is then passed forward to Sharetribe as _idpToken_ parameter. Now we'll need to expose login endpoints that invoke functions provided by the `github.js` file. In `server/apiRouter.js`, add the following import: ```jsx filename="server/apiRouter.js" const { authenticateGithub, authenticateGithubCallback, } = require('./api/auth/github'); ``` And after all the `router.*` invocations, add Github login routes: ```jsx filename="server/apiRouter.js" // This endpoint is called when the user wants to initiate authentication with Github router.get('/auth/github', authenticateGithub); // This is the route for callback URL the user is redirected after authenticating // with Github. In this route a Passport.js custom callback is used for calling // loginWithIdp endpoint in Sharetribe API to authenticate user to Sharetribe router.get('/auth/github/callback', authenticateGithubCallback); ``` Finally, on the server side we need to update `server/api/auth/createUserWithIdp.js` so that a correct IdP client ID is passed to the Sharetribe API. In the beginning of the file resolve the following environment variables: ```js filename="server/api/auth/createUserWithIdp.js" const GITHUB_PROXY_CLIENT_ID = process.env.GITHUB_PROXY_CLIENT_ID; const GITHUB_PROXY_IDP_ID = process.env.GITHUB_PROXY_IDP_ID; ``` And update the logic that resolves the `idpClientId` variable: ```js filename="server/api/auth/createUserWithIdp.js" const idpClientId = idpId === FACEBOOK_IDP_ID ? FACEBOOK_APP_ID : idpId === GOOGLE_IDP_ID ? GOOGLE_CLIENT_ID : idpId === GITHUB_PROXY_IDP_ID ? GITHUB_PROXY_CLIENT_ID : null; ``` ### Add a Github login button to Sharetribe Web Template Once we have added the authentication endpoints to the template server, we need to add a button for Github login to the AuthenticationPage. We can once more use the existing Google and Facebook login code as an example. You will need to add a _showGithubLogin_ prop and pass it down alongside _showFacebookLogin_ and _showGoogleLogin_, and add _showGithubLogin_ to the _showSocialLogins_ logic so that the button gets shown correctly. Then, create a function _authWithGithub_ similar to _authWithGoogle_ and _authWithFacebook_, which adds the default URL parameters to the API call and then redirects user to the authentication endpoint. ```jsx filename="SocialLoginButtons.js" const authWithGithub = () => { const { baseUrl, queryParams } = getDataForSSORoutes(); window.location.href = `${baseUrl}/api/auth/github?${queryParams}`; }; const githubAuthenticationMessage = isLogin ? ( ) : ( ); ``` Then we can use the _SocialLoginButton_ component to show the option to log in with Github to the users. Remember to add the Github related marketplace text keys as well as the Github logo too! Usually, different identity providers have brand centers where you can find the logos and guidelines how to use them. You can download the Github logo from [Github's site](https://github.com/logos). ```jsx filename="AuthenticationPage.js" { showGithubLogin ? (
authWithGithub()}> {githubAuthenticationMessage}
) : null; } ``` In the `AuthenticationPage` component, the `idp` const defines what is presented as the name of the identity provider in the sign up confirm page. By default, it uses the IdP ID stored in a cookie with a capitalized first letter. In case that is not sufficient approach given the IdP ID in use, a custom name for the identity provider can be used by, for example, by comparing the IdP ID in the cookie to the one used by your proxy IdP and overriding the default when suitable. That's it! In order to integrate some other identity provider, implement their authentication flow using Passport.js or some other method and use the utility functions in `api-util/idToken.js` accordingly to wrap the login information into an OpenID Connect ID token that can be used to log in to a Sharetribe marketplace. ## Troubleshooting your integration If your integration is not working as you would expect, we recommend that you check these things: ### Inspect the id token You can log the `idpToken` created with the createIdToken function, and then paste it to [JWT.io](https://jwt.io/) to investigate what data is being included in the token. Make sure that any claims you are sending correspond to the [Open ID Connect standard claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims). If there are claims missing, make sure that you are passing valid parameters [when creating the token in idToken.js](https://github.com/sharetribe/web-template/blob/main/server/api-util/idToken.js#L58-L63). ### Make sure your .well-known endpoints are available in the correct address Check that your deployed application exposes **[your-marketplace-url]/.well-known/openid-configuration** and **[your-marketplace-url]/.well-known/jwks.json** endpoints, and that **[your-marketplace-url]** exactly matches the IdP URL defined for your identity provider client in Console ### Double check your RSA key formatting Your RSA keys need to be correctly formatted in your deployment: - Include the `-----BEGIN ... KEY-----` and `-----END ... KEY-----` lines in your environment variables - If you are passing a secret file (such as with Render), format the key into a single string and replace line breaks with `\n` - If you are passing a multi-line environment variable (such as with Heroku), copy the key with line breaks as-is. - If you are running the template via ngrok and serving the RSA keys through environment variables, you might encounter an issue where the raw \n character is not treated as a newline character. This can lead to errors when trying to read the private or public key. To fix this, you can replace the raw \n character with the actual newline character. You can achieve this by using `key.split(String.raw`\n`).join('\n')` --- ## Developer documentation for Sharetribe Path: index.mdx LandingPageCard, CustomCardGrid, } from '../app/components/CustomCards/CustomCards'; Tutorials and guides to help get you up to speed Developer Documentation Sharetribe is designed from the ground up to be extended with code. These docs explain how your marketplace works under the hood, and how you can adapt it to fit your unique idea. --- ## Build with AI Path: introduction/build-with-ai/index.mdx # Build with AI Sharetribe provides an AI assistant in our developer documentation. You can use the related MCP server to connect our documentation to your AI coding assistant. In this guide, you will learn how to connect the documentation MCP server to your coding assistant. ## Documentation MCP server The documentation MCP server uses information from the same sources as the documentation AI assistant. - Dev Docs - API and SDK references - Sharetribe Developer Blog - Sharetribe's Github repositories for the Sharetribe Web Template and Integration API examples - selected articles from the Sharetribe Help Center ### Available tools The MCP server exposes one tool: ``` search_sharetribe_knowledge_sources ``` This tool initiates a search of the information sources listed above, and returns the most relevant content for the query. Results are returned as a structured list of objects with: - `source_url` – the URL of the original source. - `content` – the chunk content in Markdown. ### Setup When you open the Sharetribe developer docs AI assistant, you can see a button in the top right corner with the text "Use MCP". Clicking this button will show you shortcuts to add the documentation MCP server to Cursor, VS Code, and Claude Code, as well as copy the MCP server URL `https://sharetribe.mcp.kapa.ai` to use the MCP server in other tools. ![MCP setup shortcuts](./docs-use-mcp.png) Setup steps vary depending on which client you are using. {/* prettier-ignore */} Run the following command in your terminal: ``` claude mcp add --transport http sharetribe-docs https://sharetribe.mcp.kapa.ai ``` Then run the `/mcp` command in Claude Code, and follow the steps in your browser to authenticate. For more information, see the [Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp). Add to your Claude Desktop config file: **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` ```shell filename="claude_desktop_config.json" { "mcpServers": { "sharetribe-docs": { "command": "npx", "args": ["mcp-remote", "https://sharetribe.mcp.kapa.ai"] } } } ``` Restart Claude Desktop for changes to take effect. For more details, see the [Claude Desktop documentation](https://support.anthropic.com/en/articles/9487310-desktop-app). ChatGPT Desktop supports MCP servers in developer mode: 1. Open ChatGPT Desktop. 2. Go to Settings > Features. 3. Enable Developer mode. 4. Navigate to Settings > MCP Servers. 5. Click Add Server and enter: - Name: `sharetribe-docs` - URL: `https://sharetribe.mcp.kapa.ai` For more information, see the [ChatGPT Developer mode documentation](https://platform.openai.com/docs/guides/developer-mode). Add the following to your .cursor/mcp.json file: ```shell filename=".cursor/mcp.json" { "mcpServers": { "sharetribe-docs": { "type": "http", "url": "https://sharetribe.mcp.kapa.ai" } } } ``` For more information, see the [Cursor MCP documentation](https://docs.cursor.com/context/model-context-protocol). Prerequisites: VS Code 1.102+ with GitHub Copilot enabled. Create an `mcp.json` file in your workspace `.vscode` folder: ```shell filename=".vscode/mcp.json" { "servers": { "sharetribe-docs": { "type": "http", "url": "https://sharetribe.mcp.kapa.ai" } } } ``` For more details, see the [VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/customization/mcp-servers). MCP is an open protocol supported by many clients. Use the server URL `https://sharetribe.mcp.kapa.ai` and refer to your client's documentation for setup instructions. Most clients accept the standard MCP JSON configuration format: ``` { "mcpServers": { "sharetribe-docs": { "url": "https://sharetribe.mcp.kapa.ai" } } } ``` {/* // prettier-ignore */} And that's it! You're ready to use the Sharetribe documentation MCP server to speed up your AI assisted development! --- ## Design files Path: introduction/design-files/index.mdx # Design files ![Template in Figma](./template-figma.png) ## [ Download the design files](https://www.figma.com/community/file/1609226500517799928) The Sharetribe Web Template designs are available as Figma files. These files are the starting point for your custom marketplace design and using them can dramatically speed up your design process. ## Design files We created the design files in [Figma](https://www.figma.com). You might need to download the Inter font from [Google Fonts](https://fonts.google.com/specimen/Inter). Once you have the design files installed, you can start working on customizing the design to match the visual identity of your marketplace. Not only should you concentrate on how your marketplace should look but also on what kind of user interfaces are required for your users to interact with each other. If you do not want to do the design work yourself, you can always hire a designer to do it for you. If you don't know any designers, contact [Sharetribe support](mailto:hello@sharetribe.com), and we'll connect you to a designer who can help you. The design files are under the [Creative Commons licence](https://creativecommons.org/licenses/by/4.0/). [ Download the Design Files for Figma](https://www.figma.com/community/file/1609226500517799928) --- ## introduction/development-skills/index.mdx Path: introduction/development-skills/index.mdx # What development skills are needed with the Sharetribe Web Template? You can use any technology to build a marketplace on top of the [Marketplace API](/introduction/#the-marketplace-api). However, making a marketplace user interface (UI) from scratch requires a lot of effort. This is why we provide the Sharetribe Web Template. It is a polished marketplace web application that is ready for customization. The Sharetribe Web Template is a template web application that uses the Marketplace API. It is built using common and modern frontend tooling, so frontend developers should feel right at home and happy with all the technology. ## Technologies Here are the main technologies the Sharetribe Web Template uses: - JavaScript: the programming language for the whole application - CSS: styling the user interface using [CSS Modules](https://github.com/css-modules/css-modules) - [React](https://reactjs.org/): library for creating user interfaces with components - [Redux](https://redux.js.org/): state and data flow handling - [React Router](https://reactrouter.com/en/main): routing - [Final Form](https://github.com/final-form/final-form): forms - [Express](https://expressjs.com/): server side rendering of the React application - [Node.js](https://nodejs.org/): development tooling and running the Express server ## Customization In addition to the technologies used in the template, customizing the template with code will likely require skills with the following tools: - Working with the [Sharetribe APIs](/concepts/api-sdk/marketplace-api-integration-api/) and the [corresponding SDKs](/concepts/api-sdk/js-sdk/) - Command line tools like the [Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/) - [The EDN notation format](/concepts/development/edn/) for modifying transaction processes and migration files ## Development tooling While the Sharetribe Web Template uses Webpack, PostCSS, and various other tools, knowledge of these technologies is not needed in usual customizations. Sharetribe Web Template has a default Webpack setup (based on the now deprecated [Create React App](https://github.com/facebook/create-react-app) library) with server-side rendering, code-splitting, and CSS modules functionality. In most cases you do not need to make changes to this setup. The [config/README.md file](https://github.com/sharetribe/web-template/tree/main/config/README.md) in the root directory of the repository has more information on the webpack setup, in case you have a use case for modifying the configurations. These use cases might include The configurations defined in the `config/` directory are used in the following scripts (defined under the `scripts/` directory): - `scripts/build.js` - `scripts/build-server.js` - `scripts/start.js` - `scripts/test.js` ## Getting help If you are unsure if something is possible or how to do a certain thing with Sharetribe, you can contact our support: [hello@sharetribe.com](mailto:hello@sharetribe.com). For developers doing customizations, we have an active community in Slack: - [Sharetribe Developers Slack](https://www.sharetribe.com/dev-slack) --- ## Getting started with the Integration API Path: introduction/getting-started-with-integration-api/index.mdx # Getting started with the Integration API The Sharetribe Integration API is an application programming interface that provides full access to the marketplace's data. It can be used to build applications that integrate different own or 3rd party systems with the Sharetribe marketplace. For an overview of the different APIs that Sharetribe provides, read [this article](/concepts/api-sdk/marketplace-api-integration-api/). In this tutorial, you will download, set up and run an [example Integration API command line script](https://github.com/sharetribe/integration-api-examples) that will report some data about your Sharetribe marketplace. ## Install development tooling To get the example script up and running, you will need to download and install some basic development tooling: - [Git](https://git-scm.com/downloads) - [Node.js](https://nodejs.org/) - [Yarn](https://classic.yarnpkg.com/en/docs/install) ## Clone the examples Git repository Clone the examples repository: ```bash git clone https://github.com/sharetribe/integration-api-examples.git ``` Go to the cloned directory: ```bash cd integration-api-examples ``` ## Install the dependencies Install all dependencies: ```bash yarn install ``` ## Create Integration API application in Sharetribe Console The example scripts use the Sharetribe Integration SDK for JavaScript. In order for the SDK to be able to authenticate to the Integration API, it requires two values: a client ID and a client secret. You can obtain both by creating a new [Integration API application](/concepts/development/applications/) in Sharetribe Console. Log in to your marketplace in [Sharetribe Console](https://console.sharetribe.com/) and navigate to [Build > Applications](https://console.sharetribe.com/advanced/applications). ![Applications in Sharetribe Console](./apps.png) Click the `Add new` link, fill in an application name (for instance "My example integration") and choose `Integration API` as the API. ![Create a new application](./create-app.png) Once the application is created, you will see a screen listing your application's client ID and client secret. Keep that screen open, as you will need these values in the next step. ![Example application client ID and client secret details screen](./app-data.png) Always keep your client secret secure. Never expose it to an untrusted device or application, such as an end user's browser or mobile app. ## Configuration Copy the environment configuration template file: ```bash cp env-template .env ``` Open the `.env` file in your favorite text editor and fill in the `SHARETRIBE_INTEGRATION_CLIENT_ID` and `SHARETRIBE_INTEGRATION_CLIENT_SECRET` variables with the values you obtained in the previous step. ## Run an example report You can get a summary report for your marketplace listings, users and transactions running the following example: ```bash node scripts/analytics.js ``` You should see output similar to this: ``` ================ My Marketplace analytics ================ Listings: 80 - 4 draft(s) - 5 pending approval - 70 published - 1 closed Users: 150 Transactions: 25 This month, starting from Sun Dec 01 2019: - 3 new user(s) - 10 new listing(s) - 9 new transaction(s) ``` ## Next steps - Study the examples [source code](https://github.com/sharetribe/integration-api-examples) to get a better understanding on how to use the Integration SDK - Read the [Integration API reference documentation](https://www.sharetribe.com/api-reference/) - Start building your own integration. You can do so in different ways, depending on your needs: - Build your integration as a backend feature on top of your existing Sharetribe Web Template app - Fork our Integration API examples repository and build on top of that - Create a completely new application. If you use JavaScript as your programming language, you can make use of our Integration SDK. Otherwise, you will need to implement similar functionality as the SDK already provides in your own language of choice. --- ## Getting started with the JavaScript SDKs Path: introduction/getting-started-with-sdks/index.mdx # Getting started with the JavaScript SDKs Sharetribe's [JavaScript Software Development Kit](https://sharetribe.github.io/flex-sdk-js/) (SDK) and [Integration SDK](https://sharetribe.github.io/flex-integration-sdk-js/) simplify interactions with our APIs. You can read more about features and functionalities in our [SDK GitHub Documentation](https://sharetribe.GitHub.io/flex-sdk-js/index.html). This guide covers - Installation - Initialization and instantiating the SDK - Creating a trusted SDK instance - Example use cases ## Overview Our SDKs simplify making requests to the backend by handling authentication and providing convenient access to Sharetribe's APIs. They allow you to interact easily with - [Marketplace API](https://www.sharetribe.com/api-reference/marketplace.html), for interacting with marketplace data where the actor is the end user - [Integration API](https://www.sharetribe.com/api-reference/integration.html), for building integrations within the marketplace where the actor is a server or service - [Asset Delivery API](https://www.sharetribe.com/api-reference/asset-delivery-api.html), for delivering assets You can find more information on API usage and guidelines for choosing which to work with in [this article](https://www.sharetribe.com/docs/concepts/api-sdk/marketplace-api-integration-api/#when-to-use-the-marketplace-api). There are two JavaScript SDKs you can use to access the APIs
Sharetribe SDK Sharetribe Integration SDK **Access level** Limited to user-scope and public data Full marketplace (operator-level) access **Actor** Marketplace user Server, script, Zapier, etc. **API access** Marketplace API and Asset Delivery API Integration API **Client credentials** Client ID, Client secret for trusted SDK instances Client ID, Client Secret **Secure runtime environment needed** When initialised with a Client Secret Yes Throughout this article, you'll find that we use the term 'Trusted SDK instance'. In practice, this means an instance of the SDK that holds a trusted user access token. This takes part after performing a token exchange, which we will cover [later in this article](#initializing-the-sharetribe-sdk-with-a-trusted-user-access-token). To try out the SDKs, visit our [Github documentation for the Sharetribe SDK](https://sharetribe.GitHub.io/flex-sdk-js/index.html) and the [Integration SDK](https://sharetribe.github.io/flex-integration-sdk-js/) for instructions on how to get started. For a more detailed guide on using the SDKs with code, keep following this tutorial. Certain requests will a secure runtime environment. One way to do this is to run a small server from which to make these types of requests. ## Installation To get started with the SDKs, you'll first need to install them with either [Yarn](https://yarnpkg.com/) or [npm](https://www.npmjs.com/). {/* prettier-ignore */} ```shell yarn add sharetribe-flex-sdk yarn add sharetribe-flex-integration-sdk ``` ```shell npm install sharetribe-flex-sdk npm install sharetribe-flex-integration-sdk ``` {/* // prettier-ignore */} ## SDK Initialization Overview ### Obtaining the Client ID and Client Secret To use the Marketplace API or Integration API, you will need a **Client ID** from an application. You can create an application in Sharetribe Console: _Build > Advanced > Applications_ ![Sharetribe Console: Applications tab](./console-build-application.png) You should also make note of the **Client Secret**, as some requests require initializing the SDK with both the Client ID and Client Secret. An example of this type of request is making a privileged transition in a transaction. This requires that the: - Client ID and Client Secret are used when creating an instance of the SDK - A token exchange happens to obtain a trusted user access token - The request is made from a secure runtime environment, such as a server In the [Sharetribe Web Template](https://github.com/sharetribe/web-template), these types of requests are made on a small Node.js server included in the Template. ### Initializing a Sharetribe SDK instance You can initialize a Sharetribe SDK instance using only the application's Client ID. Ensure the Client ID is from a **Marketplace API application** created in Console. ```js const sharetribeSdk = require('sharetribe-flex-sdk'); const sdk = sharetribeSdk.createInstance({ clientId: '', }); ``` This will allow you to make requests that don't require authentication e.g. querying listings. ### Initializing an Integration SDK instance If you are accessing Integration API, you'll need to also provide the Client Secret. Ensure the Client ID is from an **Integration API application** created in Console. ```js const sharetribeIntegrationSdk = require('sharetribe-flex-integration-sdk'); const integrationSdk = sharetribeIntegrationSdk.createInstance({ clientId: '', clientSecret: '', }); ``` Keep your application's Client Secret secure. Never expose it to untrusted devices or applications such as end user browsers or mobile apps. ### Initializing the Sharetribe SDK with a trusted user access token To make certain requests, you will need to access the Marketplace API with a trusted SDK instance. In order to do this, you'll need to log in using the SDK and perform a token exchange to retrieve a trusted token. {

Create an instance of the SDK with the Client ID, Client Secret, and Token Store

} ```js const sharetribeSdk = require('sharetribe-flex-sdk'); var sdk = sharetribeSdk.createInstance({ clientId: '', clientSecret: '', }); ``` Keep your application's Client Secret secure. Never expose it to untrusted devices or applications such as end user browsers or mobile apps. {

Log in using valid credentials

} ```js sdk.login({ username: '', password: '' }); ``` {

Exchange the user access token for a trusted user access token and create a new instance of the SDK

} ```js sdk.exchangeToken().then((res) => { const sdkTrusted = sharetribeSdk.createInstance({ clientId: '', tokenStore: sharetribeSdk.tokenStore.memoryStore(res.data), }); }); ``` Now you can use `sdkTrusted` to make requests that require a trusted user access token, e.g. deleting a user with [Marketplace API](https://www.sharetribe.com/api-reference/marketplace.html#delete-user). Requests that require a trusted user access token rely on valid login credentials appropriate for the request. For example, to delete a user you must log in with that user's credentials. ## Example usage Here are a few examples demonstrating common uses and capabilities of the SDKs. ### Logging in **API**: Marketplace API Some endpoints require that a user is logged in first, e.g. creating listings, viewing their own listings, or initiating inquiry transactions. ```js const sharetribeSdk = require('sharetribe-flex-sdk'); var sdk = sharetribeSdk.createInstance({ clientId: '', }); sdk .login({ username: '', password: '' }) .then((_) => { // Request dependent on an authenticated user access token }); ``` ### Initiating a transaction **API**: Marketplace API The process of initiating a transaction is highly variable because it depends entirely on transaction flow customizations and the required parameters of the transition. For this example, we'll use the default-booking process, and choose `transition/request-payment` as the first step. This requires that: - The customer is logged in - A token exchange is made to get a trusted user access token - The request includes the following parameters - `transition` - `processAlias` - `params` - `listingID` - `bookingStart` - `bookingEnd` - `lineItems` This example uses the **minimum** required parameters per the transition defined in the transaction process. You'll likely want to include more information during this step, e.g. updating the transaction's extended data. See the [Marketplace API reference documentation](https://www.sharetribe.com/api-reference/marketplace.html#initiate-transaction) for more information. ```js const sharetribeSdk = require('sharetribe-flex-sdk'); const transition = 'transition/request-payment'; const processAlias = 'default-booking/release-1'; const params = { listingId: '', bookingStart: '2025-12-20T22:00:00.000Z', bookingEnd: '2025-12-21T22:00:00.000Z', lineItems: [ { code: 'line-item/night', unitPrice: { _sdkType: 'Money', amount: 5000, currency: 'EUR', }, quantity: 1, includeFor: ['customer', 'provider'], }, { code: 'line-item/provider-commission', unitPrice: { _sdkType: 'Money', amount: 5000, currency: 'EUR', }, percentage: -13, includeFor: ['provider'], }, ], }; const sdk = sharetribeSdk.createInstance({ clientId: '', clientSecret: '', }); sdk .login({ username: '', password: '' }) .then((_) => { sdk.exchangeToken().then((res) => { var sdkTrusted = sharetribeSdk.createInstance({ clientId: '', tokenStore: sharetribeSdk.tokenStore.memoryStore(res.data), }); // It's useful to first call initiateSpeculative to validate the data and to ensure // the real request will complete as expected sdkTrusted.transactions .initiateSpeculative( { transition, processAlias, params, }, { expand: true, } ) .then((speculatedResult) => { if (speculatedResult.status == 200) { sdkTrusted.transactions .initiate({ transition, processAlias, params, }) .then((initiateResult) => { // initiateResult.data }); } }); }); }); ``` Keep your application's Client Secret secure. Never expose it to untrusted devices or applications such as end user browsers or mobile apps. ### Transitioning a transaction **API**: Marketplace API This example shows how you can transition a transaction that has already been initiated. The logged-in user must be a party that is allowed to perform the transition - either the customer or provider. This can be checked in the transaction process visualizer in Console, or from the transaction's `process.edn` file. Not all transitions require the use of a trusted SDK instance. For non-privileged transitions, simply [logging in](#logging-in) is sufficient. For privileged transitions however, you'll need to use a trusted SDK instance. You can read more about privileged transitions [here](https://www.sharetribe.com/docs/concepts/transactions/privileged-transitions/). ```js const sharetribeSdk = require('sharetribe-flex-sdk'); var sdk = sharetribeSdk.createInstance({ clientId: '', clientSecret: '', }); sdk .login({ username: '', password: '' }) .then((_) => { sdk.exchangeToken().then((res) => { var sdkTrusted = sharetribeSdk.createInstance({ clientId: '', tokenStore: sharetribeSdk.tokenStore.memoryStore(res.data), }); sdkTrusted.transactions .transition({ id: '', transition: '', params: {}, }) .then((res) => { // res.data }); }); }); ``` ### Updating metadata in a transaction **API**: Integration API Updating metadata for a transaction requires that you use the Integration API. ```js const integrationSdk = require('sharetribe-flex-integration-sdk'); var sdk = integrationSdk.createInstance({ clientId: '', clientSecret: '', }); sdk.transactions .updateMetadata({ id: '', metadata: { '': '', }, }) .then((res) => { // res.data }); ``` ### Updating a listing **API**: Marketplace API Updating a listing requires that the listing author's credentials are used. ```js const sharetribeSdk = require('sharetribe-flex-sdk'); var sdk = sharetribeSdk.createInstance({ clientId: '', }); sdk .login({ username: '', password: '' }) .then((_) => { sdk.ownListings .update({ id: '', description: '', privateData: { '': '', }, publicData: { '': '', }, }) .then((res) => { // res.data }); }); ``` ### Retrieving listing assets defined in Console **API**: Asset delivery API This example will show how to retrieve all listing-related configurations as defined in Console. - Types - Categories - Fields - Search ```js const sharetribeSdk = require('sharetribe-flex-sdk'); var sdk = sharetribeSdk.createInstance({ clientId: '', }); sdk .assetsByAlias({ paths: [ '/listings/listing-types.json', '/listings/listing-categories.json', '/listings/listing-fields.json', '/listings/listing-search.json', ], alias: 'latest', }) .then((res) => { // res.data }); ``` ## Additional resources For the complete documentation on the JavaScript SDKs, visit our Github documentation for the [Sharetribe SDK](https://sharetribe.github.io/flex-sdk-js/index.html) and the [Integration SDK](https://sharetribe.github.io/flex-integration-sdk-js/). There, you'll find - Features - Configuration options - Additional examples - How to use the browser or the [API Playground](https://sharetribe.GitHub.io/flex-sdk-js/try-it-in-the-playground.html) (only available for the Sharetribe SDK) for easy testing --- ## Getting started with Sharetribe CLI Path: introduction/getting-started-with-sharetribe-cli/index.mdx # Getting started with Sharetribe CLI Sharetribe CLI (Command-line interface) is a tool for changing your marketplace's advanced configurations such as transaction processes and email templates. For this tutorial you should have basic knowledge of command-line and how to run basic commands. Now, let's get started! ## Install Sharetribe CLI Sharetribe CLI is distributed via [npmjs](https://www.npmjs.com/package/flex-cli). To install packages from npmjs, you will need to download and install Node.js development environment: - [Node.js](https://nodejs.org/) - [Yarn](https://classic.yarnpkg.com/en/docs/install) When you have installed Node.js and Yarn, type the following command to install Sharetribe CLI: ```bash yarn global add flex-cli ``` **NOTE:** If you are working with Yarn Modern, use this command instead: ```bash yarn dlx add flex-cli ``` To verify that Sharetribe CLI was successfully installed, run: ```bash flex-cli ``` This command should show you the CLI version and list available commands. Didn't work? Have a look at the [Troubleshooting](#troubleshooting). ## Help `flex-cli help` is the command to see the list of available commands: ```bash flex-cli help ``` In order to see subcommand help, pass the command as an argument for the `flex-cli help` command. For example, let's see help for command `login`. ```bash flex-cli help login ``` ## Get an API key To log in you need to have a personal API key. To get an API key, log in to Console then click your email address in the bottom left of the sidebar and click [Manage API keys](https://console.sharetribe.com/api-keys). ## Log in After you've received [your API key](#get-an-api-key), you can log in with `flex-cli login` command. First, let's see help for the login command: ```bash flex-cli help login ``` You can see that the command does not require any additional options. So let's run it: ```bash flex-cli login ``` The command will prompt you your API key. After successful log in, you will be greeted by your admin email address. Once logged in, you can work with any marketplace that you have been granted access to. ## List processes and process versions Now that you have successfully logged in and know how to use the `help` command, let's use CLI to list processes and process versions in your marketplace. The command to list processes is `process list`. Let's see the help first: ```bash flex-cli help process list ``` As you can see, the command requires `MARKETPLACE_ID` option. You can use either the long form `--marketplace ` or short form `-m `. You can find the Marketplace ID from [Sharetribe Console](https://console.sharetribe.com/) on the [Build > General](https://console.sharetribe.com/general) page. Optionally the command takes `--process PROCESS NAME` parameter to get detailed information about a single process. Let's list all the processes: ```bash flex-cli process list -m my-marketplace-dev ``` This command shows you a list of transaction processes in your marketplace. ## Summary In this tutorial, we installed Sharetribe CLI, logged in using an API key and tried some example commands. In addition, we familiarized ourselves with the `help` command that is the main source of documentation for the Sharetribe CLI. We also learned how to disable and enable the listing approval functionality for our marketplace. Now that we know how to list processes, the next this is to [make a small change to the existing process](/how-to/transaction-process/edit-transaction-process-with-sharetribe-cli/). ## Troubleshooting ### flex-cli: command not found If you're seeing `flex-cli: command not found` error and you installed Sharetribe CLI with Yarn, you need to add [Yarn global bin path to the PATH environment variable](https://classic.yarnpkg.com/en/docs/cli/global/#adding-the-install-location-to-your-path). #### On Windows 1. Run `yarn global bin` to see the global bin path 2. Add it to PATH environment variable 3. Restart command line For a step-by-step guide with screenshots, have a look at this blog post: ['yarn global add' command does not work on Windows](https://sung.codes/blog/2017/12/30/yarn-global-add-command-not-work-windows/) #### On Mac 1. Open your shell configuration file. If you are using _zsh_, it will be `.zshrc`. 2. Add `export PATH="$(yarn global bin):$PATH"` to the config file. 3. Restart terminal --- ## Getting started with Sharetribe Web Template Path: introduction/getting-started-with-web-template/index.mdx # Getting started with Sharetribe Web Template The Sharetribe Web Template is a marketplace web application built on top of the [Marketplace API](/introduction/#the-marketplace-api). While you can create a marketplace purely using just the API, it requires a significant amount of effort (both money and time) and we recommend using the template as a starting point for customizations. The Sharetribe Web Template is built with [React](https://reactjs.org/), [Redux](https://redux.js.org/), and [CSS Modules](https://github.com/css-modules/css-modules). It also contains a small [Node.js](https://nodejs.org/en/) server, which provides server-side rendering (SSR) for the deployed site. The purpose of this guide is to clone and configure the Sharetribe Web Template to your local development environment - and then get it up and running. This guide also helps you to create accounts to Stripe and Mapbox. Those services are needed to run the Sharetribe Web Template app. We recommend that you take the steps for this guide in your Sharetribe Console dev environment. [Read more about Sharetribe environments](/concepts/development/sharetribe-environments/). ## Setup a development environment ### Prerequisities To get Sharetribe Web Template up and running, you will need to download and install some basic development tooling: - [Git](https://git-scm.com/downloads) - [Node.js](https://nodejs.org/) - [Yarn](https://classic.yarnpkg.com/en/docs/install) ### Install the Sharetribe Web Template App locally 1. Open Terminal 2. [Clone](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) the Sharetribe Web Template repository: ```bash git clone https://github.com/sharetribe/web-template.git ``` 3. Go to the cloned directory: ```bash cd web-template/ ```
Check how the directory structure should look like After these steps you should have a directory structure that looks like this for Sharetribe Web Template: ```bash ├── ext │   └── transaction-processes ├── node_modules │ └── // dependencies ├── public │   ├── static │   ├── index.html │   ├── robots.txt │   └── 500.html ├── scripts │   ├── audit.js │   ├── config.js │   └── translations.js ├── server │   ├── api │   ├── api-util │   ├── apiRouter.js │   ├── apiServer.js │   ├── auth.js │   ├── csp.js │   ├── dataLoader.js │   ├── env.js │   ├── importer.js │   ├── index.js │   ├── log.js │   ├── renderer.js │   ├── sitemap.js │   └── wellKnownRouter.js ├── src │   ├── analytics │   ├── app.js │   ├── app.node.test.js │   ├── app.test.js │   ├── assets │   ├── components │   ├── config │   ├── containers │   ├── context │   ├── ducks │   ├── examples.js │   ├── index.js │   ├── reducers.js │   ├── routing │   ├── store.js │   ├── styles │   ├── transactions │   ├── translations │   └── util ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json └── yarn.lock ```
4. Install dependency libraries: ```bash yarn install ``` ### Check that you have the correct transaction processes in your environment If you have created your marketplace environment prior to the 25th of April 2023, and you are using the Sharetribe Web Template, it is good to note that there are two new transaction processes the template uses, and those processes may not be in your Sharetribe marketplace by default. You can find the transaction processes in [/ext/transaction-processes/](https://github.com/sharetribe/web-template/tree/main/ext/transaction-processes) in the repository. To use the template, you will need to have the transaction processes in your Sharetribe environment. [Follow these steps](https://github.com/sharetribe/web-template#take-the-new-beta-processes-into-use) to create both processes in your environment through Sharetribe CLI. ## Mandatory Integrations The Sharetribe Web Template has 3 mandatory integrations that you need to configure before the app is fully functional: - Stripe - a map provider for location services - Sharetribe Marketplace API ![Mandatory integrations: Sharetribe Marketplace API, Stripe, Map provider](./web-template-customizations.png) One of the integrations is configured in Console, and two are configured with environment variables. So the Sharetribe Web Template needs these three environment variables to make mandatory integrations work: - **[`REACT_APP_SHARETRIBE_SDK_CLIENT_ID`](#sharetribe-client-id-and-client-secret)** - **[`SHARETRIBE_SDK_CLIENT_SECRET`](#sharetribe-client-id-and-client-secret)** - **[`REACT_APP_STRIPE_PUBLISHABLE_KEY`](/introduction/set-up-and-use-stripe/)** ### Stripe The Sharetribe Web Template uses [Stripe](https://stripe.com/en-fi) as a payment processor, and Sharetribe Web Template saves sensitive payment information directly to Stripe. First, follow these instructions to get your Stripe keys. Add your Sandbox secret key to Console, and make a note of your Sandbox publishable key. We will set that publishable key as the value of **REACT_APP_STRIPE_PUBLISHABLE_KEY** later in this guide. - [Set up Stripe for a custom developed marketplace](/introduction/set-up-and-use-stripe/) ### A map provider You can use either [Mapbox](https://www.mapbox.com) or [Google Maps](https://developers.google.com/maps/) to provide location search (geocoding) and maps for the web app. You can configure your map provider in Console by following these instructions: - [How to set up Mapbox or Google Maps for location services](https://www.sharetribe.com/help/en/articles/8676185-how-to-set-up-mapbox-or-google-maps-for-location-services) You can make map access tokens in your web applications more secure by adding URL restrictions. When you add a URL restriction to a token, that token will only work for requests that originate from the URLs you specify. See the Mapbox documentation for domain restrictions: - [Token management – URL restrictions](https://docs.mapbox.com/accounts/guides/tokens/#url-restrictions). ### Sharetribe client ID and client secret To use the Marketplace API, you will need a **client ID**. You can [sign up for your free Sharetribe account here](https://console.sharetribe.com/new). When you get access, you will be able to log into Sharetribe Console and check the client ID.
Sharetribe Console: _Build > Advanced > Applications_ In addition, Sharetribe Web Template uses transaction processes that include privileged transitions. This makes it possible to customize pricing on the Node server that's included in the template. The **client secret** is needed to make this secure call from the template's own server to Sharetribe API. ![Sharetribe Console: Applications tab](./console-build-application.png) ## Add Environment Variables Start the config script: ```bash yarn run config ``` This command will prompt you to enter the three required environment variables that you you collected in the previous step. After that, it will create `.env` file to your local repository and guide you through setting up the rest of the required environment variables. If the `.env` file doesn't exist the application won't start. _This `.env` file is only created for the local development environment_. See the [template environment variables](/template/configuration/template-env/) for more information on the environment variables. ## Start the server Start the development server: ```bash yarn run dev ``` Running `yarn run dev` uses [Webpack's dev-server](https://webpack.js.org/configuration/dev-server/) with [Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/). This will automatically open `http://localhost:3000` in a browser: ![Default marketplace screenshot](./generic-landingpage.png) The Sharetribe Web Template fetches no-code content and configurations from the Sharetribe backend using the [Asset Delivery API](https://www.sharetribe.com/api-reference/asset-delivery-api.html). Read more about how no-code assets are handled between the Sharetribe Console and the client application in our [asset reference documentation](/references/assets/). As you browse your marketplace and create listings, you may notice that the search filters do not work. You can activate the filters by creating a [search schema](/how-to/search/manage-search-schemas-with-sharetribe-cli/#adding-schemas) that corresponds to your template. ## Summary In this tutorial, we used the Sharetribe Web Template to get a marketplace running. Here's a summary of those installation steps: ```bash git clone https://github.com/sharetribe/web-template.git cd web-template/ yarn install yarn run config yarn run dev ``` As you can see from `http://localhost:3000`, Sharetribe Web Template is a fully ready and polished marketplace application that is running on top of the Marketplace API. Client app customization is in your control, and you can change it to fit your marketplace needs. Check the [tutorial](/tutorial/) to learn how to customize the Sharetribe Web Template. --- ## Introducing Sharetribe Developer Platform Path: introduction/index.mdx # Introducing Sharetribe Developer Platform ## Introduction Sharetribe is a complete solution for building a powerful online marketplace for rentals, services, events or experiences. Sharetribe provides you with all the necessary marketplace infrastructure out of the box. At the same time, because of its headless architecture, Sharetribe Developer Platform gives you total freedom to customize your user experience. ## Sharetribe Components ![Sharetribe customer architecture](./sharetribe-customer-architecture.png) ### Your marketplace UI Your marketplace user interface (UI), also known as the "front end", is what your users see and interact with. They use the UI to sign up to your platform as well as to post, find, book, and pay for listings. Whether a web-based UI, a mobile application, or both, the user interface is the face of your solution. That's why you have total control over it. You can fully choose things like which fonts and colors to use in your user interface, what kind of pages or views there should be, and how those should be laid out. There are no limits to what you can do in terms of visual design. [Sharetribe Web Template](/introduction/getting-started-with-web-template/) is a web template that implements a full-feature marketplace experience powered by Sharetribe. The template can be configured to multiple types of marketplaces out of the box: - a rental marketplace with daily or nightly bookings - a service marketplace with time-based bookings - a product marketplace with stock management - a messaging marketplace with no payments - a reverse or regular gig marketplace with price negotiation With a Sharetribe Web Template, you can get started developing your marketplace UI straight away: just download the latest version and start customizing it for your marketplace. There are no limits to how much you can customize the template. You can freely design the user interactions and alter the look and feel of your marketplace. You can also integrate any web analytics or customer service solutions directly into your UI. Your users won't see Sharetribe mentioned anywhere. You can set the web address of your custom developed marketplace to your own domain, the email notifications get sent from your email address, and the entire user experience is fully tailored to match your brand. If you prefer to start from scratch, you can build your own UI on top of Sharetribe's Marketplace API yourself. For instance, if you want to build a mobile application for your marketplace, you will need to build a fully custom user interface. You can also have several UI applications for the same marketplace, so the user can manage their listings and transactions on a native mobile app as well as on the web. ### The Marketplace API The Marketplace API is how your UI connects to the Sharetribe Developer Platform services. It's an HTTP interface with a design influenced by the [JSON API specification](https://jsonapi.org/) and the [CQRS pattern](https://martinfowler.com/bliki/CQRS.html). The Marketplace API allows you to implement all the standard marketplace functionality that Sharetribe supports. Sharetribe handles running and scaling the Marketplace API. To take full advantage of this, you should design your Marketplace UI to point a majority of the traffic directly to the Marketplace API. For example, Sharetribe Web Template only handles the initial page load when a user opens their browser. After that, the template lets the client application (Single-page application) talk directly with the Marketplace API, and the API powers all further interactions. To learn more about the capabilities of the Marketplace API and Sharetribe Developer Platform, visit the [API reference documentation](/concepts/api-sdk/api/). ### Sharetribe JavaScript SDKs [The Sharetribe JavaScript SDKs](/concepts/api-sdk/js-sdk/) are small JavaScript libraries that help you with integrating the Marketplace API, Asset delivery API, and Integration API. It handles tasks like authentication and session management and makes it easy to use correct data types with the API. Using the Sharetribe JS SDKs is not required, but if you are working with JavaScript, we strongly encourage you to have a look. Check out our guide on [getting started with the SDKs](/introduction/getting-started-with-sdks/) for an overview on how to start working with them, common use cases, and examples. ### Console [Console](https://console.sharetribe.com/) is where you to manage all your marketplace data, such as users, listings and transactions. Console also offers tools to develop and customize your marketplace, for example, an editor for email templates. Console is accessed via a web UI that we provide out of the box. Think of it as an admin interface you never have to build! ### The Integration API The Integration API is how you can integrate third party solutions to your Sharetribe marketplace. It provides full access to your marketplace data and operations, so you can integrate a vast range of services to your marketplace behind the scenes. To read more about the capabilities of the Integration API, visit the [API reference documentation](/concepts/api-sdk/api/). You can also read more about [integrations in Sharetribe](/concepts/integrations/integrations-introduction/). ### The Asset Delivery API The Asset Delivery API is how you can fetch your no-code content and configurations, defined in Console, to your client application. To read more about the capabilities of the Asset Delivery API, visit the [Asset Delivery API reference documentation](https://www.sharetribe.com/api-reference/asset-delivery-api.html). You can also read more in our [asset reference article](/references/assets/). ### The Authentication API The Authentication API is how you authenticate your API requests. All requests to Marketplace API and Integration API require a valid token, and tokens can provide different levels of access depending on their type. If you are using the Javascript SDKs, authentication is handled within the SDK tooling. To read more about the capabilities of Authentication API, visit the [API reference documentation](https://www.sharetribe.com/api-reference/authentication.html) or read more in our [Authentication API article](/concepts/api-sdk/authentication-api/). ### Sharetribe CLI Sharetribe CLI (Command-line interface) is a tool for changing your marketplace's advanced configurations such as transaction processes and email templates. See the [Getting started with Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/) guide for more information. --- ## Start customizing your marketplace Path: introduction/introduction-to-customizing/index.mdx # Start customizing your marketplace When you start customizing your marketplace with code, you may want to continue working on the same web template that powers your hosted marketplace, or you may want to develop a fully custom application. Both options are possible, and there are some considerations for both approaches. ## General custom development guidelines Whichever custom development approach you choose, here are some key points that apply to all custom development. ### Get familiar with the Sharetribe APIs Your marketplace basic functions are available through the **Marketplace API**. Your no-code configurations are available through the public **Asset Delivery API**. Capabilities for creating custom integrations and data analytics are available server-side through **Integration API**. Authentication to Marketplace API and Integration API happens through the **Authentication API**. Access to the Sharetribe APIs in the live environment is available in the Extend plan. The Marketplace API, Asset Delivery API, and Integration API are also accessible through JavaScript SDKs. - [Read more about Marketplace API and Integration API](/concepts/api-sdk/marketplace-api-integration-api/) - [Read more about Asset Delivery API](/references/assets/) - [Read more about Authentication API](/concepts/api-sdk/authentication-api/) - [Read more about Sharetribe JavaScript SDKs](/concepts/api-sdk/js-sdk/) - [Read more about Sharetribe pricing](https://www.sharetribe.com/pricing/) ### Get familiar with Sharetribe environments Your marketplace has three environments – Test, Dev, and (eventually) Live. We recommend that you only do development work in Dev, and keep Test and Live identical in terms of code. Each marketplace environment connects to the corresponding client app through a Marketplace API application, i.e. a client id and a client secret. This means that when you call the Marketplace API or Asset Delivery API, the correct marketplace environment is determined by the client id set in your app. - [Read more about Sharetribe environments](/concepts/development/sharetribe-environments/) - [Read more about Sharetribe applications](/concepts/development/applications/) ### Get familiar with hosting your application When you custom develop your marketplace, you also need to host your own application. When starting your custom development, it is enough to host your custom client application for the Dev environment. This makes it possible for several people to test and validate the features you are developing. In addition, some custom features, such as third-party integrations, may require a hosted environment to work properly. When you want to publish your code, you will also need to host a client application for the Test and Live environments. Hosting your custom client app for Test environment allows operators to view their no-code changes alongside the custom developed features, so it is important to keep Test and Live client applications identical. Once you have deployed your marketplace application to a hosting service, you can change the Marketplace URL in your Console > Build > General view to point to the URL of your custom client. That way, the Sharetribe backend uses the correct URL for e.g. adding links to emails sent through the Sharetribe backend. ## Custom developing with the Sharetribe Web Template If you want to continue developing your marketplace based on the Sharetribe-hosted client app, you can use the Sharetribe Web Template as your starting point. Sharetribe Web Template is an open-source version of the code that powers your Sharetribe-hosted marketplace. - [Read more about the default features of Sharetribe marketplaces](https://www.sharetribe.com/features/) The template uses the Sharetribe SDK to connect to the Marketplace API and Asset Delivery API. The SDK also handles authentication towards Marketplace API, and shares the user's login between the browser and the server. - [Read more about the Sharetribe SDK](https://sharetribe.github.io/flex-sdk-js/) To get started with the template, you need to clone the Sharetribe Web Template repository and set it up with your marketplace client id and secret. The template uses your no-code configurations, fetched with the SDK, to render a marketplace that looks similar to your Sharetribe-hosted marketplace. You can then continue to custom develop features in your cloned codebase. - [Read more about getting started with the Sharetribe Web Template](/introduction/getting-started-with-web-template/) The Sharetribe Web Template repository contains the starting point for your development work. To maintain version control of your own development work, you need to create your own remote repository for your cloned version of the template. The tutorial guides you through some common features that you can modify in the Sharetribe Web Template to custom develop your marketplace. We recommend that you check it through for an overview on the different customizations that are possible with the Sharetribe Developer Platform. - [Read the tutorial and learn about setting up a remote repository for your cloned codebase](/tutorial/) To share your changes online, you will need to deploy the template. Note that to deploy the template, for any marketplace environment (Dev, Test, or Live), your deployment service needs to be able to run a Node.js server. - Hosting environments that support Node.js servers: - [Heroku](https://www.sharetribe.com/docs/template/hosting/how-to-deploy-template-to-heroku/) - [Render](https://www.sharetribe.com/docs/tutorial/deploy-to-render/) - [Fly.io](https://fly.io/docs/js/) - [Microsoft Azure](https://learn.microsoft.com/en-us/azure/app-service/quickstart-nodejs?tabs=linux&pivots=development-environment-vscode) - [Containerized environments](https://www.sharetribe.com/docs/template/hosting/run-template-with-docker/) - Hosting environments that do not support Node.js servers: - Vercel (supports front-end technologies and Next.js) - AWS Amplify (supports front-end technologies and Next.js) * [Read more about template deployment](/template/hosting/how-to-deploy-template-to-production/) ## Custom developing with your own client application You can also create a fully custom client application for your marketplace. The headless architecture of the Sharetribe Developer Platform allows you to build fully custom. You might want to build a mobile application, integrate Sharetribe marketplace functionalities to your existing website infrastructure, or simply develop with the stack of your choice. Do note that currently we only have SDKs available for Javascript. You can check our guide on [getting started with the Sharetribe SDKs](/introduction/getting-started-with-sdks/) for a non-Template focused approach- We also recommend that you check through the template tutorial for an overview on what customizations are possible with the Sharetribe Developer Platform. - [Read the tutorial](/tutorial/) When deciding on the stack for your custom client application, it is good to note that to fully use the features of the Sharetribe Developer Platform, you need some kind of a server environment. The server is necessary for features that require a trusted non-browser context for security reasons. Features requiring a server include: - using SSO and identity providers - Integration API – it provides full access to your marketplace, so it must never be used from a browser environment - privileged transitions, including custom pricing - logging in as a user from Console * [Read more about using SSOs with a Sharetribe marketplace](/concepts/users-and-authentication/social-logins-and-sso/) * [Read more about Integration API security](https://github.com/sharetribe/integration-api-examples#warning-usage-with-your-web-app--website) * [Read more about privileged transitions](/concepts/transactions/privileged-transitions/) * [Read more about Login as user](/concepts/users-and-authentication/login-as-user/) --- ## Set up Stripe for a custom developed marketplace Path: introduction/set-up-and-use-stripe/index.mdx # Set up Stripe for a custom developed marketplace Both Sharetribe API and your client app need to be able to connect with Stripe API. Stripe has two different keys: - _Secret key_ for server-side requests - _Publishable key_ for calls from web browser Sharetribe API uses the Stripe secret key to make payment-related requests when a transaction moves forward. The client app needs to use the Stripe publishable key to run the `stripe.js` script. The script has two main functions: it has fraud detection built in, and it is also used to save sensitive information directly to Stripe. For instance, a customer's credit card number is saved directly to Stripe. ## Setting up Stripe for your custom developed marketplace First, [follow these instructions](https://www.sharetribe.com/help/en/articles/8413086-how-to-set-up-stripe-for-payments-on-your-marketplace#h_1d4b6c331e) to set up your Stripe account. If you already have a Live environment, you have most likely completed these steps already. When setting up your marketplace for a custom developed app, you will need to add the Stripe secret key in your Console, and your publishable key in your custom app. You need to do this setup separately for each environment that you are connecting with a custom developed app. ### Get your Stripe keys Next, retrieve your Stripe API keys in your [Stripe Dashboard API Keys tab](https://dashboard.stripe.com/test/apikeys). For your Test and Dev environments, you will use Sandbox keys, and for your Live environment, you will use Live keys. Sandbox keys start with **pk_test** (publishable key) and **sk_test** (secret key). Live keys start with **pk_live** (publishable key) and **sk_live** (secret key). See Stripe's explanation of key types: - [API keys – Key types](https://docs.stripe.com/keys#obtain-api-keys) ### Add your secret key to your Console Next, you need to add your secret key in your Console. - Log in to Console and go to Build > Integrations > Payments. - In the section "Stripe configuration", add your secret key to the "Stripe secret key" field and save your changes. ![Add Stripe secret key](./stripe_dev_key_console.png) The secret key and publishable key need to match each other. You can't use a publishable key from a different Stripe account than the secret key - or mix test keys and live keys. ### Add your publishable key to your application In your client application, you need to use Stripe publishable key when you are making API calls to Stripe. If you are using the Sharetribe Web Template, the default calls to Stripe API are already there, but you need to add the Stripe publishable key to your .env file. You can do this in two ways: - by running _yarn run config_ - or editing the .env file directly in a text editor The environment variable for setting the Stripe publishable key is **REACT_APP_STRIPE_PUBLISHABLE_KEY**. Read more about configurations in the Sharetribe Web Template in [Template environment variables](/template/configuration/template-env/). --- ## Assets Description: Reference documentation providing information on assets. Path: references/assets/index.mdx # Assets _Assets_ are a mechanism for defining client application configuration data and content for a marketplace. The assets are managed by a marketplace operator and change relatively infrequently. Each _asset_ is an object that defines a _path_ and _content_. The path gives the asset a name and allows organizing assets in a way similar to a file system, where assets can be organized in directory-like structure. The content of the asset is its data in one of the supported data types. At present, Sharetribe supports only JSON data as assets. Support for images (JPEG, PNG, etc) is coming in the future. The assets are typically edited by a marketplace operator though Sharetribe Console. All assets are considered public and must not be used to store secret or otherwise sensitive information. At present, Sharetribe uses assets for managing marketplace content without code changes: - you can modify [marketplace text strings](/concepts/content-management/marketplace-texts/), - create content [pages](/concepts/content-management/content-management-in-sharetribe/) for your marketplace, and - configure your marketplace branding, layout, users, listings, search, footer, and transactions For example, a marketplace may have the following assets: ```shell ├── content │ └── translations.json └── design └── branding.json ``` where `design/branding.json` could contain configuration data (for instance, UI colors, logo, etc) and `content/translations.json` could contain string marketplace text data to be used in a marketplace client application (such as one based on one of the Sharetribe Web Template). ## Asset versioning Collectively, the set of assets of a marketplace is called the _asset tree_. Each time the data of one or more assets is changed, a new asset is added, or an existing asset is deleted, a new version of the asset tree is created. Old asset tree versions are never updated. In other words, tree versions are immutable. Each tree version, therefore, represents the exact set of assets and their data at the time when the version was created. The following analogy with the Git version control system may be useful to consider: The asset tree versions are analogous to Git commit SHAs. The entire asset tree is versioned as a whole and individual assets do not have their own independent versions. Unlike Git, however, the asset tree versioning does not support branching. To facilitate access to the latest asset data, Sharetribe maintains a built-in _alias_ called `latest` that always refers to the latest asset tree version. Old versions of the asset tree may be automatically deleted, but no sooner than 24 hours after the version gets succeeded by a newer one. ## Retrieving asset data Client applications retrieve asset data through the [Asset Delivery API](https://www.sharetribe.com/api-reference/asset-delivery-api.html). Asset data can be access either by alias (using the built-in `latest` alias) or by specific version. In order to access asset data, clients need a client ID for a valid Sharetribe Marketplace API application. The easiest way to access assets is by using the [Sharetribe SDK for JavaScript](/concepts/api-sdk/js-sdk/#sharetribe-sdk-for-javascript). See the [Asset Delivery API reference](https://www.sharetribe.com/api-reference/asset-delivery-api.html) for more information. ### Asset data caching In order to ensure as efficient data retrieval as possible, the Asset Delivery API response data can be cached by both the Asset CDN and the client. Each API response comes with appropriate `Cache-Control` and `ETag` HTTP headers automatically and caching works out of the box with clients that support these headers (such as the end users' web browsers). Since asset versions are immutable, asset data that is accessed by a specific version can be cached for extended period of time. On the other hand, the `latest` alias is mutable and therefore asset data retrieved by alias cannot be cached indefinitely. The cache time for access by alias can differ depending on whether your marketplace environment is a development or live one. For Live marketplaces the cache time can be up to 5 minutes, while for Test and Development marketplaces it is much lower. Refer to the [Asset Delivery API reference](https://www.sharetribe.com/api-reference/asset-delivery-api.html) for up-to-date-information. The `Cache-Control` HTTP header will always provide correct data and client applications should observe that if custom caching is being implemented. In live marketplaces, the latest asset data can be cached and it may take up to 5 minutes before any changes are visible to all end users. ## Available default asset files Assets are fetched by file path. The asset files available to be fetched by default are the following: ### General - Localization: `/general/localization.json` - Access control: `/general/access-control.json` ### Content - Content pages: `/content/pages/{pageId}.json` - Top bar: `/content/top-bar.json` - Footer: `/content/footer.json` - Marketplace texts: `/content/translations.json` - Email texts: `/content/email-texts.json` ### Design - Branding: `/design/branding.json` - Layout: `/design/layout.json` ### Users - User types: `/users/user-types.json` - User fields: `/users/user-fields.json` ### Listings - Listing types: `/listings/listing-types.json` - Listing categories: `/listings/listing-categories.json` - Listing fields: `/listings/listing-fields.json` - Listing search: `/listings/listing-search.json` ### Transactions - Minimum transaction size: `/transactions/minimum-transaction-size.json` ### Monetization - Commission: `/transactions/commission.json` ### Integrations - Map: `/integrations/map.json` - Analytics: `/integrations/analytics.json` - Google Search Console: `/integrations/google-search-console.json` ## Further reading - [Editing client application marketplace texts](/concepts/content-management/marketplace-texts/) - [Managing asset-based marketplace content](/concepts/content-management/content-management-in-sharetribe/) - [Asset Delivery API reference](https://www.sharetribe.com/api-reference/asset-delivery-api.html) --- ## Listing availability management Path: references/availability/index.mdx # Listing availability management The listing availability management features of Sharetribe allow listing authors to define when (and when not) their listings are available for booking. There are three different concepts related to availability management that together define whether a certain time or day is available on not: - An **availability plan** can be defined for each listing. It comprises of general availability rules for each day of the week. For instance "available on Mondays and Thursdays", or "available on Tuesday, Wednesday and Friday from 9 AM to 6 PM". - An **availability exception** overrides the availability plan for a concrete period of time. For instance "Available on 2018-11-25 and 2018-11-26", "not available on 2018-11-30". - A **booking** represents a reservation (or an intention to make a reservation) for a concrete period of time. The availability plan and exceptions, together with booking information can be combined to determine if a particular time range is available for booking or not. For instance, the `/timeslots/query` API endpoint returns availability information for future dates, taking into account the listing's availability plan, exceptions and bookings. All bookings are created through transactions, governed by your [transaction process](/concepts/transactions/transaction-process/). The transaction process ensures that bookings are only created for available time ranges. ## Seats Both availability plans and availability exceptions use the concept of _seats_ to define whether a particular time is available or not. How many seats a booking consumes depends on the `seats` attribute of a booking. Having 0 seats in availability means that the listing is unavailable for bookings during that time. ## Day-based availability management Day-based availability works with both daily and nightly bookings. For instance, an availability plan can define that Mondays and Tuesdays are available for booking. For daily bookings this means that dates that are a Monday or a Tuesday can be booked. For nightly bookings, this means that nights Monday-Tuesday and Tuesday-Wednesday can be booked. ### Interpretation of availability exceptions and bookings For day-based availability plans, it is recommended to create availability exceptions with timestamps having `00:00:00` time in UTC. Creating availability exceptions with arbitrary time is allowed, but such exceptions are subject to the following interpretation rules in the context of a listing with day-based availability plan: - if the availability exception covers only partially a given date in UTC time zone, the availability exception is interpreted as **covering the entire date** - if multiple availability exceptions cover partially a given UTC date, the **minimum number of seats** of all these availability exceptions is taken as the resulting number of available seats for that date, prior to taking any existing bookings into account. If your transaction process uses time-based bookings, the bookings are also subject to the same interpretation rules. **Example 1:** An exception with start `2018-11-26T12:30:00.000+01` and end `2018-11-27T10:25:00.000+01` is interpreted as if it were from `2018-11-26T00:00:00.000Z` to `2018-11-28T00:00:00.000Z` **Example 2:** An exception with start `2018-11-26T00:30:00.000+01:00` and end `2018-11-27T00:15:00.000+01:00` is interpreted as if it were from `2018-11-25T00:00:00.000Z` to `2018-11-27T00:00:00.000Z`. **Example 3:** An exception with start `2018-11-26T00:30:00.000+01:00` and end `2018-11-27T15:15:00.000+01:00` is interpreted as if it were from `2018-11-25T00:00:00.000Z` to `2018-11-28T00:00:00.000Z`. ## Time-based availability management Time-based availability can be used with time-based bookings. Time-based availability plans can specify one or more time intervals for each day of the week, and specify the time zone in which these times should be interpreted. For instance, with time-based availability it is possible to define that the listing is available on weekdays from 9 AM to 11AM and from 1 PM to 6 PM. ### Timeslots, availability plans and exceptions Timeslots are periods of time, which are available to be booked. E.g. if you have set a plan with 1 available seat from 7-22 on Mondays, that means that one person can book 5 minutes `07:00` - `07:05`. Then the next customer can only book times within the range of `07:05` - `22:00` on the same day. So, the plan creates a weekly schedule, against which availability exceptions and bookings are making reservations. Exceptions can be used for restricting availability on a specific day. E.g. if you have set availability to `07` - `22` on Mondays and you add an exception `21-22` with seat `0` on Monday `28.10.2019`, the timeslots query returns timeslot `07-21` on that day if there are no bookings. Exceptions can also be used for expanding the availability on a specific day. E.g. if you have set availability to `07` - `22` on Mondays and you add an exception `22-23` with seat `1` on Monday `28.10.2019`, the timeslots query returns timeslot `07-23` on that day if there are no bookings. For time-based plans, both availability exceptions and bookings are interpreted literally, i.e. covering the exact time intervals determined by their start and end times. ### Interval based filtering Interval based filtering allows querying timeslots by partitioning the queried time frame (start to end) into smaller sub-intervals, and then matching and filtering timeslots based on whether timeslots are contained within the sub-intervals. This can be particularly useful for checking day-by-day availability over an extended period for fixed-duration bookings. Interval based filtering is supported by the `/timeslots/query` endpoint. It is controlled via four query parameters: - `intervalDuration`: the length of each sub-interval (e.g. `"PT30M"`, `"P1D"`) - `maxPerInterval`: the maximum number of timeslots to return per sub-interval - `minDurationStartingInInterval`: minimum required duration of a timeslot starting in an interval, in minutes - `intervalAlign`: optional timestamp to control where intervals start (defaults to start time of the query) `intervalDuration` partitions the queried time frame into smaller sub-intervals, which is provided as an [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). `maxPerInterval` then limits the number of time slots matched within each sub-interval. Only a number of results up to the defined maximum are returned, with excess slots being excluded. `minDurationStartingInInterval` sets a threshold (in minutes) that determines whether a particular time slot counts toward the `maxPerInterval` limit. The threshold value is compared to the duration of the time slot, cut to the start of the current sub-interval. `intervalAlign` is a timestamp anchoring the starting point of these sub-intervals, giving control over how intervals align relative to the query start. Here are four examples of different scenarios illustrating how the filtering logic works. All examples use `intervalDuration: P1D`, `maxPerInterval: 1` and `minDurationStartingInInterval: 100`, i.e. 1 hour and 40 minutes. Therefore, any timeslot that is at least a 100 minutes long and starts within the interval should be returned **Example 1:** Only Timeslot 1 is returned by the query, as it is the first matching timeslot within "Day 1". ![Example 1 of how the API returns timeslots using interval-based filtering](./example1.png) **Example 2:** Both timeslots are returned by the query. ![Example 2 of how the API returns timeslots using interval-based filtering](./example2.png) **Example 3:** In this example, we have Timeslot 1 that spans both sub-intervals. The timeslot overlaps at least a 100 minutes within both sub-intervals, so it is the first matching timeslot in both "day 1" and "day 2". Timeslot 2 is not returned, as we have specified `maxPerInterval: 1` in the query parameters. ![Example 3 of how the API returns timeslots using interval-based filtering](./example3.png) **Example 4:** Here, both Timeslot 1 and Timeslot 2 are returned by the query. Even though Timeslot 1 only overlaps within "Day 1" for 60 minutes, it's total length is 2 hours, i.e. 120 minutes. As long as a timeslot starts within an interval, it does not have to be fully contained to the interval. The whole duration of the timeslot is compared to the `minDurationStartingInInterval: 100` value. Timeslot 1 does not overlap with "Day 2" for 100 minutes or over (it overlaps for 60, as the slot ends at `01:00:00`). The comparison is cut to the start of the current sub-interval. Therefore, the first timeslot that matches the conditions is Timeslot 2, and both timeslots are returned by the query. ![Example 4 of how the API returns timeslots using interval-based filtering](./example4.png) ## Booking states A booking can be in one of several possible states: `pending`, `proposed`, `accepted`, `canceled` or `declined`. Bookings change state only through a corresponding transaction transition, using one of the [booking-specific actions](/references/transaction-process-actions/#bookings). All bookings in `pending` or `accepted` states count as reservation against the listing's availability. On the other hand, bookings in the `proposed`, `canceled` or `declined` states do not affect the availability of the listing. The figure below illustrates the possible booking states, transitions between the states and the corresponding actions that you can use in your transaction process. ![Booking states](./booking-states.png) In addition, the `:action/update-booking` action can be used to update a booking's details (start or end times, seats) when the booking state is either `proposed`, `pending` or `accepted`. The booking remains in the same state as it was before the update. New bookings are always created in either `pending` or `proposed` state. As `pending` bookings reserve availability, they are useful when your transaction process allows customers to immediately reserve their spot (often combined with a preauthorization of a payment). Using `proposed` bookings is useful in situations where multiple users should be allowed to request to book certain time range independently of one another. The listing author would be able to choose which request(s) to accept (it may be possible to accept more than one request, if there are enough available seats), as long as the listing has sufficient remaining availability. ## Booking display times Booking display times are a handy tool for managing listing availability. You can use them by passing `bookingDisplayStart` and `bookingDisplayEnd` attributes when creating or updating a booking. This will set `displayStart` and `displayEnd` attributes correspondingly to the booking that is related to the initiated transaction. The display times can be used alongside with the regular `start` and `end` attributes (defined by `bookingStart` and `bookingEnd` params) of a booking and they can be used to present different start and end times to the customer than actually is booked. See [the booking resource format](https://www.sharetribe.com/api-reference/marketplace.html#booking-resource-format) for a full list of booking attributes. **Example:** A provider needs 10 minutes of preparation time before each booking. They can pass the following params regarding booking start when initiating a transaction: ```json bookingStart: "2018-04-20T12:20:00.000Z", bookingDisplayStart: "2018-04-20T12:30:00.000Z" ``` The `displayStart` attribute will now indicate that the booking starts at _12:30_ and this can be presented to the customer. However, the listing is booked already from _12:20_, denoted by booking's `start` attribute. Now the listing is not available for other bookings 10 minutes before this booking starts. If another customer wishes to book this listing earlier, their booking will end at latest 10 minutes before this booking. ## Related API endpoints See the reference documentation for the following API endpoints for details: - [/own_listings/create](https://www.sharetribe.com/api-reference/marketplace.html#create-listing) - [/own_listings/update](https://www.sharetribe.com/api-reference/marketplace.html#update-listing) - [/availability_exceptions/](https://www.sharetribe.com/api-reference/marketplace.html#availability-exceptions) - [/timeslots/query](https://www.sharetribe.com/api-reference/marketplace.html#query-time-slots) - [/transactions/initiate](https://www.sharetribe.com/api-reference/marketplace.html#initiate-transaction) --- ## Digital files management Description: Reference documentation for digital files management. Path: references/digital-files/index.mdx # Digital files management With Sharetribe's digital files management features, listing authors can upload and download digital files and attach them to other resources. There are three key concepts related to digital files management: - `file` is the Sharetribe representation of the uploaded digital file item. - `ownFile` is the Sharetribe representation of the uploaded digital file item visible to the author of the upload. - `fileAttachment` is the association of a `file` to another marketplace resource, such as a message. The actual digital file entity is uploaded to and downloaded from Sharetribe storage with a direct URL, not through the Sharetribe APIs. The URLs are managed through three other resource types: - `fileUpload` contains the URL and other details for uploading a file directly to Sharetribe storage. - `fileDownload` contains the URL for downloading a file directly from Sharetribe storage. - `ownFileDownload` contains the URL for downloading a user's own uploaded file directly from Sharetribe storage. ## File states A file can be in one of several possible states: - `pendingUpload` - `pendingVerification` - `available` - `verificationFailed` Files change state as a result of the file verification process that happens once a user uploads the digital file entity using the signed upload URL. Download URLs are only generated for files in `available` state. ## File attachment scopes A `file` resource can be associated with another resource by adding it to the other resource's file attachments. The scope of the file attachment determines the visibility of the file: - `publicFileAttachments` are visible to all users who have access to the resource in question - `protectedFileAttachments` are visible to the user who created the resource, and additionally the file can be revealed in a transaction to the other transaction participant - `privateFileAttachments` are visible only to the user who created the resource. The [API reference](https://www.sharetribe.com/api-reference/) details which resources have which scopes of file attachments available. ## Further reading - Marketplace API reference - [Files](https://www.sharetribe.com/api-reference/marketplace.html#files) - [Own files](https://www.sharetribe.com/api-reference/marketplace.html#own-files) - [File attachments](https://www.sharetribe.com/api-reference/marketplace.html#file-attachments) - [File uploads](https://www.sharetribe.com/api-reference/marketplace.html#file-uploads) - [File downloads](https://www.sharetribe.com/api-reference/marketplace.html#file-downloads) - [Own file downloads](https://www.sharetribe.com/api-reference/marketplace.html#own-file-downloads) - Integration API Reference - [Files](https://www.sharetribe.com/api-reference/integration.html#files) - [File attachments](https://www.sharetribe.com/api-reference/integration.html#file-attachments) --- ## Email templates Path: references/email-templates/index.mdx # Email templates Sharetribe supports customizing the contents of all the emails that are sent from the platform. The platform sends two types of emails: - **built-in emails** on events like email changed or user joined - **transaction emails** as notifications as part of the transaction process. The built-in emails can be customized using the [Built-in email notifications editor](https://console.sharetribe.com/advanced/email-notifications) in the Sharetribe Console. You find the editor in Console under **Build** > **Advanced** > **Email notifications** section. To change the transaction emails, follow the [Edit email templates with Sharetribe CLI](/how-to/emails-and-notifications/edit-email-templates-with-sharetribe-cli/) tutorial. ## Best practices In addition to the basic branding of the emails to follow your marketplace brand and visual guidelines, we recommend that you follow these best practises for email branding to avoid spam folders: - Make the email personal, add a starting line with the name of the recipient and other distinct elements like the title of the listing. - Add a common footer to the emails with basic information about your marketplace. - Currently, the emails sent from the platform are purely notifications on actions in the marketplace, there isn't a way to unsubscribe from these emails, so make sure your users have given consent to receiving these emails when registering to the platform. - Design your emails so that they encourage users to navigate to the marketplace and continue the actions there - we don't support e.g. responding to messages in the platform through email. Sharetribe's email system handles SPF (Sender Policy Framework) and DKIM (DomainKeys Identified Mail) automatically to improve your email deliverability. ## Handlebars The template language used to build the email templates is called [Handlebars](https://handlebarsjs.com/). With Handlebars you get direct access to the HTML that will be sent to your users. The Handlebars template defined by you will be applied to the email `context`, that contains the data for the email, e.g. recipient name, marketplace name and all the transaction details such as booking dates, line items etc. if the email is related to a transaction. The result of applying the email context to the template is the rendered HTML that will be sent to your users. Please read through the [Handlebars](https://handlebarsjs.com/) documentation for more information about the templating language. ## Helpers Sharetribe email templating supports a subset of [Handlebars built in helpers](https://handlebarsjs.com/guide/builtin-helpers.html). In addition to the built-in helpers, we have implemented a small set of custom helpers that make e.g. comparisons and number formatting possible. The helpers may support positional parameters, hash parameters or both. For example: `{{helper param-1 param-2 hash-param=value}}`. Some documentation on the syntax and how to use them can be found in the documentation for [Handlebars expressions](https://handlebarsjs.com/guide/expressions.html). ### Built-in helpers We support the following built-in helpers: - `each` - `with` - `if` - `unless` Have a look at the [Handlebars built-in helpers documentation](https://handlebarsjs.com/guide/builtin-helpers.html) to see examples how to use them. In addition to those, we also support [inline partials](https://handlebarsjs.com/guide/partials.html#inline-partials). ### Custom helpers This paragraph lists all the custom helpers we provide, including the parameters they take and example how to use them: #### `asset` **Parameters**: - asset path - optional, JSON key or dot-separated nested keys - optional, default value The helper is used to retrieve data from assets. It can locate values within nested fields by providing a sequence of keys, separated by dots, to traverse the JSON structure. For example, the colour of a button in the email template can be assigned dynamically by accessing the `design/branding.json` asset. The use of this helper is not limited to the branding asset – any asset data can be used in the templates. ```handlebars {{asset 'design/branding.json' 'marketplaceColors.notificationPrimaryButton' '#007DF2' }} ``` In addition, in the email templates, this helper is used to define the correct localization settings in the email templates without modifying the template structure itself using the [`set-translations`](#set-translations) and [`set-locale`](#set-locale) helpers. #### `concat` **Parameters**: - ...strings - Any number of string literals and/or variables to concatenate Example usage: ```handlebars {{concat 'Hello ' customer.display-name '!'}} {{concat marketplace.url '/sale/' (url-encode id) '/'}} {{concat 'color:' primaryColor ';font-size:16px'}} ``` Inline helper that concatenates (joins) multiple strings and variables into a single string. No separators are automatically added between arguments, so you need to include spaces or other separators within your string literals where needed. The helper accepts any number of arguments and processes them in order from left to right. Variables are resolved to their values before concatenation. Common use cases include: - Building URLs with dynamic segments - Generating dynamic translation keys - Creating inline CSS styles with dynamic color values #### `contains` **Parameters**: - collection - value Example usage: ```handlebars {{#contains collection value}}true{{else}}false{{/contains}} ``` Block helper that renders the block if `collection` has the given `value`, otherwise the else block is rendered (if specified). #### `date` **Parameters**: - time **Hash parameters:** - format - lang: optional, default: `"en-US"` - tz: optional, default `"UTC"` Example usage: ```handlebars {{date d format='d. MMM, YYYY' lang='fi-FI' tz='Europe/Helsinki'}} ``` Renders a properly localized `time` based on the `format`, `lang` and `tz` hash parameters. The `format` supports [Joda-Time formatting](https://www.joda.org/joda-time/key_format.html). The `lang` supports [IETF BCP 47](https://tools.ietf.org/html/bcp47) language tag strings. More info about language tags can be found in the [W3C Internationalization article for language tags](https://www.w3.org/International/articles/language-tags/#region). E.g. `"en-US"` is a valid string. The `tz` supports [Joda-Time time zones](https://www.joda.org/joda-time/timezones.html). #### `date-day-before` **Parameters**: - time **Hash parameters:** - format - lang: optional, default `"en-US"` - tz: optional, default `"UTC"` Example usage: ```handlebars {{date-day-before d format='d. MMM, YYYY' lang='fi-FI' tz='Europe/Helsinki' }} ``` Renders a properly localized `time`, one day before the given date, based on the `format` and `lang` hash parameters. The `format` supports [Joda-Time formatting](https://www.joda.org/joda-time/key_format.html). The `lang` supports [IETF BCP 47](https://tools.ietf.org/html/bcp47) language tag strings. More info about language tags can be found [W3C Internationalization article for language tags](https://www.w3.org/International/articles/language-tags/#region). E.g. `"en-US"` is a valid string. The `tz` supports [Joda-Time time zones](https://www.joda.org/joda-time/timezones.html). #### `date-transform` **Parameters**: - date **Hash parameters:** - days Example usage: ```handlebars {{format-text '{date,date,::EE}' date=(date-transform date days=-1)}} ``` Can be used with the [format-text](#format-text) helper to transform a date value to past or future days according to the `days` hash parameter: - negative values for transforming to the past of the date - positive values for transforming to the future of the date #### `eq` **Parameters**: - value - test Example usage: ```handlebars {{#eq value 1}}true{{else}}false{{/eq}} ``` Block helper that renders a block if `value` is **equal to** `test`. If an `else` block is specified it will be rendered when falsy. #### `form-encode` **Parameters**: - str Example usage: ```handlebars {{form-encode 'Share & Tribe'}} ``` Encode the given string as `application/x-www-form-urlencoded`. Should be used for query string components. #### `format-text` **Parameters**: - message **Hash parameters:** - list of hash parameters and their respective values used with the messages Example usage: ```handlebars {{format-text '{amount,number,::.00} {currency}' amount=money.amount currency=money.currency }} ``` Inline helper that formats a text string using the [ICU message format](https://unicode-org.github.io/icu/userguide/format_parse/messages/). This helper works similarly to the `t` helper, but instead of accepting a message key and a fallback message, it accepts a single string. Any hash parameters used inside either message must be wrapped in single curly brackets, and the values for those hash parameters need to be defined after the message. #### `inflect` **Parameters**: - count - singular - plural Example usage: ```handlebars {{inflect quantity 'day' 'days'}} ``` Returns either the `singular` or `plural` inflection of a word based on the given `count`. #### `money-amount` **Parameters**: - money **Hash parameters:** - lang: optional, default `"en-US"` Example usage: ```handlebars {{money-amount m lang='fi-FI'}} ``` Takes money and formats the amount according to the currency. For example: - EUR amounts are formatted with 2 decimals - JPY amounts are formatted with no decimals Does not output the currency code or symbol, only the amount. The `lang` supports [IETF BCP 47](https://tools.ietf.org/html/bcp47) language tag strings. More info about language tags can be found [W3C Internationalization article for language tags](https://www.w3.org/International/articles/language-tags/#region). E.g. `"en-US"` is a valid string. #### `number` **Parameters**: - number **Hash parameters:** - lang: optional, default `"en-US"` - max-fraction-digits: optional - min-fraction-digits: optional Example usage: ```handlebars {{number n lang='fi-FI'}} ``` Formats the given `number`. `lang`, `max-fraction-digits` and `min-fraction-digits` can be given as hash parameters. The `lang` supports [IETF BCP 47](https://tools.ietf.org/html/bcp47) language tag strings. More info about language tags can be found [W3C Internationalization article for language tags](https://www.w3.org/International/articles/language-tags/#region). E.g. `"en-US"` is a valid string. #### `set-locale` **Parameters**: - locale - either provided as a string or derived from an asset using the [asset](#asset) helper Example usage: ```handlebars {{set-locale 'en_US'}} {{set-locale (asset 'general/localization.json' 'locale' 'en_US')}} ``` This helper specifies how numbers, dates and currencies are formatted within the email. When providing an asset, the helper works so that it first checks the asset at the provided path for the 'locale' key. If a locale is found, the helper will extract the value and apply it to the email. Otherwise, the helper will rely on the fallback - 'en_US' in the above example. #### `set-timezone` **Parameters**: - time zone code Example usage: ```handlebars {{set-timezone 'Europe/Helsinki'}} ``` This helper applies a time zone to date/time text strings in the emails. It is used by the `format-text` function - wrapped in `format-day-time` in this case. It needs to be provided in [TZ time zone database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) format. This is consistently used in the transaction process email templates, where the time zone exists as a parameter of the listing's availability plan, for example: ```handlebars {{asset 'design/branding.json' 'marketplaceColors.notificationPrimaryButton' '#6938EF' }} ``` In addition, in the email templates, this helper is used to define the correct localization settings in the email templates without modifying the template structure itself using the [`set-translations`](#set-translations) and [`set-locale`](#set-locale) helpers. #### `set-translations` **Parameters**: - [asset](#asset) - optional, fallback asset Example usage: ```handlebars {{set-translations (asset 'content/email-texts.json')}} ``` This helper searches the (first) `asset` at the given path for the text keys defined in the email. If matching keys are found, their corresponding text is replaced with the translated versions. An optional fallback `asset` can be provided as a second parameter. If a key is not found in the primary `asset`, the helper will search the fallback `asset` and replace the text if the key is found there. #### `t` **Parameters**: - message key - fallback message **Hash parameters:** - list of hash parameters and their respective values used with the messages Example usage: ```handlebars {{t 'BookingNewRequest.Description' '{customerDisplayName} requested to book {listingTitle} in {marketplaceName}.' customerDisplayName=customer.display-name listingTitle=listing.title marketplaceName=marketplace.name }} ``` Inline helper that makes it possible to modify the email template texts without making changes in the template code. This helper uses the [ICU message format](https://unicode-org.github.io/icu/userguide/format_parse/messages/) to render messages and parameters into a string. The helper renders the message corresponding to the key, if the key exists in the [email text asset](/references/assets/). If the key does not exist, the helper renders the fallback message. Any hash parameters used inside either message must be wrapped in single curly brackets, and the values for those hash parameters need to be defined after the message key and the fallback message. #### `url-encode` **Parameters**: - str Example usage: ```handlebars {{url-encode 'Share & Tribe'}} ``` URL encodes the given string. Should be used for encoding all user input. E.g. a link to user profile with the user name in the link should be encoded. ## Editing email content For both built-in emails and transaction process emails, you can edit content with the email text editor under **Build** > **Content** > **Email texts**. This data is stored as JSON in the marketplace's public asset tree, under the "content/email-texts.json" asset/path. In the email templates, these texts are retrieved via use of the [asset](#asset) and [set-translations](#set-translations) helper functions. They can then be accessed with the [`t helper`](#t) function, which uses the text keys to insert or replace the content in the templates the corresponding values. As an example, let's look at the default booking transaction process [booking-accepted-request email](https://github.com/sharetribe/web-template/blob/main/ext/transaction-processes/default-booking/templates/booking-accepted-request/booking-accepted-request-html.html) Here is a header snippet from that template: ```handlebars

{{t 'BookingAcceptedRequest.Title' 'Your booking request was accepted' }}

``` This header uses the [`t helper`](#t) function to look up the key `BookingAcceptedRequest.Title`. If a value is defined for that key in the email texts, it will be used. If not, the fallback text "Your booking request was accepted" will be displayed instead. You can update the email templates in whatever way works best for your use case. Keep in mind when making changes using helper functions, that you include fallback texts as needed cover cases where a key may be missing from `email-texts.json`. ## Editing built-in emails The built-in emails can be customized using the [Built-in email notifications editor](https://console.sharetribe.com/advanced/email-notifications) in the Sharetribe Console. You find the editor in the Console under **Build** > **Advanced** > **Email notifications** section. In addition to the code editor that allows you to edit the template, the editor also contains a **context viewer** that shows you a sample of the context that will be used with that particular email. The editor also let's you to see a **preview** of the email before and let's you **send test emails** to your own email address. These built-in email notifications can be disabled through Console: - Listing approved - New message - User approved - User joined - User permissions changed - Verify email address ### Built-in email context variables Next to the built-in email editor in Console, you can see the different context variables and sample values that are available for the built-in email you are editing. Different email templates have different context variables available. The context variables available for each built-in template are under the following links. - [Email address changed](/references/email-templates/#email-address-changed) - [Listing approved](/references/email-templates/#listing-approved) - [New message](/references/email-templates/#new-message) - [Password changed](/references/email-templates/#password-changed) - [Reset password](/references/email-templates/#reset-password) - [User approved](/references/email-templates/#user-approved) - [User joined](/references/email-templates/#user-joined) - [User permissions changed](/references/email-templates#user-permissions-changed) - [Verify changed email address](/references/email-templates#verify-changed-email-address) - [Verify email address](/references/email-templates#verify-email-address) #### Email address changed ``` recipient: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data }, marketplace: marketplace { name: string url: string } ``` #### Listing approved ``` recipient: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } marketplace: marketplace { name: string url: string } listing: listing { id: UUID title: string private-data: extended-data public-data: extended-data 0 items metadata: extended-data 0 items availability-plan: availability-plan { type: string timezone: string } current-stock: stock { quantity: int } } ``` #### New message ``` recipient: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } marketplace: marketplace { name: string url: string } message: message { sender: user { id: UUID 5a680a01-a281-4e7a-a276-786280fda2b8 first-name: string "Alice" last-name: string "Smith" display-name: string "Alice S" private-data: extended-data 0 items public-data: extended-data 0 items protected-data: extended-data 0 items metadata: extended-data 0 items } content: string transaction: transaction { booking: booking { start: date { year: int month: int day: int hours: int minutes: int seconds: int milliseconds: int } end: date { year: int month: int day: int hours: int minutes: int seconds: int milliseconds: int } displayStart: date { year: int month: int day: int hours: int minutes: int seconds: int milliseconds: int } displayEnd: date { year: int month: int day: int hours: int minutes: int seconds: int milliseconds: int } state: string seats: int } listing: listing { id: UUID title: string private-data: extended-data public-data: extended-data metadata: extended-data availability-plan: availability-plan { timezone: string type: string } current-stock: stock { quantity: int 10 } } delayed-transition: delayed-transition { run-at: date { year: int 2018 month: int 9 day: int 27 hours: int 18 minutes: int 30 seconds: int 30 milliseconds: int 0 } } payout-total: money { amount: decimal 32.13 currency: string "USD" } protected-data: extended-data customer: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } payin-total: money { amount: decimal currency: string } id: UUID 3df61814-6b0c-4652-bbc6-869a1d911150 reviews: reviews [ 0: review { content: string "Sample review of customer" subject: user { id: UUID 5a680a01-a281-4e7a-a276-786280fda2b8 first-name: string "Alice" last-name: string "Smith" display-name: string "Alice S" private-data: extended-data 0 items public-data: extended-data 0 items protected-data: extended-data 0 items metadata: extended-data 0 items } } 1: review { content: string "Sample review of provider" subject: user { id: UUID 2b44e4fd-ec24-4730-a3ff-9af3a07d6751 first-name: string "John" last-name: string "Doe" display-name: string "John D" private-data: extended-data 0 items public-data: extended-data 0 items protected-data: extended-data 0 items metadata: extended-data 0 items } } ] tx-line-items: tx-line-items [ 0: tx-line-item { code: string unit-price: money { amount: decimal currency: string } line-total: money { amount: decimal currency: string } include-for: include-for [ 0: transaction-role "customer" 1: transaction-role "provider" ] quantity: decimal percentage: decimal units: decimal seats: int } 1: tx-line-item { code: string unit-price: money { amount: decimal currency: string } line-total: money { amount: decimal currency: string } include-for: include-for [ 0: transaction-role "provider" ] quantity: decimal percentage: decimal units: decimal seats: int } ] provider: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } stock-reservation: stock-reservation { quantity: int } state: string metadata: extended-data } } recipient-role: transaction-role ``` #### Password changed ``` recipient: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } marketplace: marketplace { name: string url: string } ``` #### Reset password ``` recipient: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } marketplace: marketplace { name: string url: string } password-reset: password-reset { token: string email-address: string } ``` #### User approved ``` recipient: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } marketplace: marketplace { name: string url: string } ``` #### User joined ``` recipient: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } marketplace: marketplace { name: string url: string } ``` #### User permissions changed ``` recipient: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } marketplace: marketplace { name: string url: string } changed-permissions: changed-permissions { read: permission-value postListings: permission-value initiateTransactions: permission-value } ``` #### Verify changed email address ``` recipient: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } marketplace: marketplace { name: string url: string } email-verification: email-verification { token: string } ``` #### Verify email address ``` recipient: user { id: UUID first-name: string last-name: string display-name: string private-data: extended-data public-data: extended-data protected-data: extended-data metadata: extended-data } marketplace: marketplace { name: string url: string } email-verification: email-verification { token: string } ``` ## Editing transaction emails To understand how to change the transaction emails, see the [Edit email templates with Sharetribe CLI](/how-to/emails-and-notifications/edit-email-templates-with-sharetribe-cli/) tutorial. ### Transaction email context Context for transaction emails: ```json { "recipient": { "id": "uuid", "first-name": "string", "last-name": "string", "display-name": "string", "private-data": "extended-data", "public-data": "extended-data", "protected-data": "extended-data", "metadata": "extended-data" }, "marketplace": { "name": "string", "url": "string" }, "recipient-role": "string", // either "provider" or "customer" "other-party": { "id": "uuid", "first-name": "string", "last-name": "string", "display-name": "string", "private-data": "extended-data", "public-data": "extended-data", "protected-data": "extended-data", "metadata": "extended-data" }, "transaction": { "id": "uuid", "state": "string", "last-transition": "string", "tx-line-items": [ { "code": "string", "unit-price": { "amount": "decimal", "currency": "string" } , "line-total": { "amount": "decimal", "currency": "string" } , "include-for": {"any-of": ["provider", "customer"]}, "quantity": "decimal", "percentage": "decimal"} ], "payout-total": { "amount": "decimal", "currency": "string" }, "booking": { "start": "date", "end": "date", "displayStart": "date", "displayEnd": "date", "seats": "integer", "state": "string" }, "stock-reservation": { "quantity": "integer", "state": "string" }, "reviews": [ { "content": "string", "subject": { "id": "uuid", "first-name": "string", "last-name": "string", "display-name": "string", "private-data": "extended-data", "public-data": "extended-data", "protected-data": "extended-data", "metadata": "extended-data" } } ], "provider": { "id": "uuid", "first-name": "string", "last-name": "string", "display-name": "string", "private-data": "extended-data", "public-data": "extended-data", "protected-data": "extended-data", "metadata": "extended-data" }, "payin-total": { "amount": "decimal", "currency": "string" }, "listing": { "id": "uuid", "title": "string," "availability-plan": { "type": "string", // either availability-plan/time or availability-plan/day "timezone": "string" }, "current-stock": { "quantity": "integer" }, "private-data": "extended-data", "public-data": "extended-data", "metadata": "extended-data" }, "customer": { "id": "uuid", "first-name": "string", "last-name": "string", "display-name": "string", "private-data": "extended-data", "public-data": "extended-data", "protected-data": "extended-data", "metadata": "extended-data" }, "delayed-transition": { "run-at": "date" }, "protected-data": "extended-data", "metadata": "extended-data" } } ``` Inside the templates there are number of properties that you can utilize when customizing the templates. What properties are available is email specific and the following data structure describes what resources are available for each email template. ### How to read the data structure - The type and content of the property can be deducted from the value of the property. E.g. `{"marketplace": {"name": "string", "url": "string"}`, is an object with properties `name` and `url` that are strings. - An object with property `"any-of"` describe an array of elements. For example, `{"any-of": ["customer", "provider"]}` is an array containing one of the values or both. - Properties of type `"date"` define a date object with properties `"year"`, `"month"`, `"day"`, `"hours"`, `"minutes"`, `"seconds"` and `"milliseconds"`. It's highly recommended that you use the available helpers described above to display dates. - Objects with the exact two properties of `"amount"` and `"currency"` are of type money, and can be passed as is to `money-amount` helper. - Properties of type `"extended-data"` define an extended data object. Properties in such an object can have any valid JSON values, including JSON data structures. - The listing `availability-plan` property `timezone` is in TZ time zone database format, for example `"Europe/Berlin"`. It is only available for plans with type `availability-plan/time`. - Remember to traverse the context properly. For example, in `"transaction-transition"` `"payin-total"` is nested under `"transaction"`. This means that the correct way to refer to that is `transaction.payin-total` or using the [builtin **`with`** helper](https://handlebarsjs.com/guide/builtin-helpers.html#with). --- ## Events Description: Reference documentation for Sharetribe marketplace events. Path: references/events/index.mdx # Events In Sharetribe, _events_ represent changes in marketplace data resources such as listings, users and transactions. An event captures a single change in marketplace data, e.g. a user being created or a listing being updated. Events can be further analyzed to interpret them as logical actions such as a listing being published, a message being sent or a user having changed their email address by looking into what were the changed data fields. **Events serve two main purposes:** 1. To allow (admin) users to observe changes in marketplace data for auditing purposes. For example, what is the history of all changes for a particular listing. 2. To allow programmatically reacting to changes and logical actions in a marketplace. Allowing programs to observe and react to changes enables efficiently solving use cases such as synchronizing changes in marketplace data into external places, e.g. a CRM, a spreadsheet or a calendar. It also allows programs to react to logical actions as part of the marketplace flows. For example, setting a field in user metadata can trigger an integration to publish the user's listings. Using events makes it possible to cover many of the use cases where other applications use webhooks. Currently, Sharetribe exposes events by allowing them to be queried via the [Integration API](https://www.sharetribe.com/api-reference/integration.html#query-events) or viewed via [Sharetribe CLI](/how-to/events/view-events-with-sharetribe-cli/). Integration API supports implementing efficient polling where only events that have happened since last poll query are returned. This makes it possible to keep the polling interval short enough to react to events shortly after they occur. Sharetribe does not retain event data forever. Sharetribe maintains a history of all marketplace events for 90 days in live marketplaces and for 7 days in dev and test marketplaces. ## Event data The event object contains data about the event itself, as well as about the Sharetribe resource which the event is about (`listing`, `user`, `message`, etc), including how the resource changed. The exact shape of the event data differ, depending on which means it is accessed with, but the information contained is the same, regardless. ### Event attributes Each event has the following attributes: | Attribute | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | (uuid) Event ID. | | `createdAt` | (timestamp) The date and time when the event occurred in ISO 8601 format. | | `sequenceId` | (integer) A numeric ID for the event that provides a strict total ordering of events, i.e. later events are guaranteed to have a sequence ID that is strictly larger than earlier events. | | `marketplaceId` | (uuid) The ID of the marketplace in which the event happened. | | `eventType` | (string) The type of the event. See [supported event types](#supported-event-types). The event type has the form `RESOURCE_TYPE/EVENT_SUBTYPE`. E.g. `listing/created`. | | `source` | (string) The Sharetribe service from which the event originated. See [event sources](#event-sources). | | `resourceId` | (uuid) The ID of the API resource that the event is about, e.g. a user or a listing ID. | | `resourceType` | (string) The type of the API resource that the event is about. This is one of the API resource types supported in the Integration API (e.g. `user`, `listing`, etc). | | `resource` | (object) The value of the resource, for which the event is about, after the event occurred. For all event types except `*/deleted` events, the `resource` attribute is populated. For `*/deleted` events, `resource` is `null`. For details see the [reference for event data and previous values](#resource-data-and-previous-values). | | `previousValues` | (object) An object describing the previous values for the event's changed resource attributes and relationships. Note that for `*/deleted` events, some of the attributes may be `null`, due to stricter data deletion requirements. For details see the [reference for event data and previous values](#resource-data-and-previous-values). | | `auditData` | (object) Data about the actor that caused the event. | | `auditData.userId` | (uuid) The ID of the Sharetribe marketplace user that caused the event, if any. This attribute is set for most events that occur in the Marketplace API and is `null` otherwise. | | `auditData.adminId` | (uuid) The ID of the Sharetribe Console admin user that caused the event. Typically set for events that occur in Console, but can be set in combination with `userId` when an admin has used the ["login as"](/concepts/users-and-authentication/login-as-user/) Console feature and acted on behalf of a marketplace user though the Marketplace API. | | `auditData.requestId` | (uuid) The ID of the API request that caused the event. Can be `null`. Currently this information is meaningless but might have future uses. | | `auditData.clientId` | (uuid) The client ID of the Sharetribe [Application](/concepts/development/applications/) that caused the event. This attribute is set if the event was caused by an API call from a Sharetribe Marketplace API or Integration API application and is `null` otherwise. | Note: these attributes are not necessarily top-level keys. Event object data structure depends on [event's data format](#event-data-formats). ### Event sequence IDs The event sequence IDs are guaranteed to uniquely identify an event. They are also strictly increasing, meaning that events that happen later always have larger sequence ID values than those that happened before them. These properties make the event sequence IDs the most accurate way to keep track of which events have been already seen or processed in an application. The query interface supports requesting events that have happened after the given sequence ID, making the sequence ID a perfect tool for loading subsequent events in comparison to a known ID. When querying events synchronously (e.g. via the [Integration API](https://www.sharetribe.com/api-reference/integration.html#query-events) or [Sharetribe CLI](/how-to/events/view-events-with-sharetribe-cli/)), events are always returned in order of their sequence IDs. Note that, in contrast to sequence IDs, there can be multiple events that have the exact same `createdAt` timestamp, so applications should not use the timestamp as a strict criteria to determining event ordering or to decide which events are processed and which are not. ### Event data formats There are two data formats for events: **native** and **Integration API** event data formats. When accessed via the Integration API, the event and associated resource data is formatted in the same way all Integration API resources are (with `id`, `type` top-level attributes and with `relationships` object containing the IDs of related resources). On the other hand, if the event is viewed via Sharetribe CLI, the event and resource data is not normalized. Instead, it is inlined in a simplified form. We will refer to this as _native_ event data format. The Integration API event data format differs from the native one as follows: - The event object itself is structured as an API resource (with `id`, `type` and `attributes` keys) - The `resource` object is structured itself as an Integration API resource with `id`, `attributes` and optional `relationships` keys, each containing the corresponding portion of resource data - The value of relationships attributes follows the same format as [relationships normally do in the Integration API](https://www.sharetribe.com/api-reference/#including-related-resources) In both formats, the `previousValues` object always has the same shape as the `resource` object. Here are two abbreviated examples (assuming that listing's title is updated): #### Native event data format ```json { "id": "ef98e897-5b81-49a5-aca6-01d9759df075", // other event keys: "eventType", "sequenceId", "createdAt", ... "resource": { "id": "5bbb2f6f-568f-470a-9949-a655e3f6ac46", //... "title": "Listing title", "author": { "id": "5cf4c0eb-513f-419b-a8be-bdb6c14be10a" } // ... }, "previousValues": { "title": "old title" } } ``` #### Integration API event data format ```json { "id": "ef98e897-5b81-49a5-aca6-01d9759df075", "type": "event", "attributes": { // other event keys: "eventType", "sequenceId", "createdAt", ... "resource": { "id": "5bbb2f6f-568f-470a-9949-a655e3f6ac46", "type": "listing", "attributes": { "title": "Listing title" // ... }, "relationships": { "author": { "data": { "id": "5cf4c0eb-513f-419b-a8be-bdb6c14be10a", "type": "user" } } // ... } }, "previousValues": { "attributes": { "title": "old title" } } } } ``` ### Resource data and previous values The following example event data is for a `listing/updated` event produced as a result of an API call in the Marketplace API. The data is in native format and its `previousValues` field indicate that the following occurred: 1. The listing `title` was updated 2. The listing `availabilityPlan` was updated 3. The `address` key in the listing's `publicData` was updated 4. The `rules` key was added to the listing's `publicData` 5. The set of listing images was updated, where 2 new images were added and one was removed For examples of the Integration API event data format, see the [Integration API reference](https://www.sharetribe.com/api-reference/integration.html#events). ```json { "id": "ef98e897-5b81-49a5-aca6-01d9759df075", "eventType": "listing/updated", "sequenceId": 12345678, "createdAt": "2020-11-27T12:30:02.000Z", "marketplaceId": "9deec37c-b59c-4884-8f60-e4944335c327", "source": "source/marketplace-api", "resourceId": "5bbb2f6f-568f-470a-9949-a655e3f6ac46", "resourceType": "listing", "resource": { "id": "5bbb2f6f-568f-470a-9949-a655e3f6ac46", "title": "Peugeot eT101", "description": "7-speed Hybrid", "deleted": false, "geolocation": { "lat": 40.64542, "lng": -74.08508 }, "createdAt": "2018-03-23T08:40:24.443Z", "state": "published", "availabilityPlan": { "type": "availability-plan/day", "entries": [ { "dayOfWeek": "mon", "seats": 1 }, { "dayOfWeek": "tue", "seats": 2 } ] }, "privateData": { "externalServiceId": "abcd-service-id-1234" }, "publicData": { "address": { "city": "New York", "country": "USA", "state": "NY", "street": "230 Hamilton Ave" }, "categoryLevel1": "road", "gears": 22, "rules": "This is a nice, bike! Please, be careful with it." }, "metadata": { "promoted": true }, "price": { "amount": 1590, "currency": "USD" }, "author": { "id": "5cf4c0eb-513f-419b-a8be-bdb6c14be10a" }, "marketplace": { "id": "9deec37c-b59c-4884-8f60-e4944335c327" }, "images": [ { "id": "209a25aa-e7cf-4967-89c3-0f09b2d482ff" }, { "id": "98e11f3b-ea22-4b1b-8549-e543ae241133" }, { "id": "ee1a647a-a751-43c7-90a4-48e94654f016" } ] }, "previousValues": { "title": "old title", "availabilityPlan": { "type": "availability-plan/day", "entries": [ { "dayOfWeek": "mon", "seats": 1 } ] }, "publicData": { "address": { "city": "New York", "country": "USA", "state": "NY", "street": "222 Hamilton Ave" }, "rules": null }, "images": [ { "id": "98e11f3b-ea22-4b1b-8549-e543ae241133" }, { "id": "d12b8ebc-4df8-4bd0-9231-2f05691831a4" } ] }, "auditData": { "userId": "5cf4c0eb-513f-419b-a8be-bdb6c14be10a", "adminId": null, "clientId": "69ea8198-201c-48c4-a3bb-78b38e4059b0", "requestId": "4b66e510-22cb-47ca-953f-8a8377af2ed0" } } ``` For all event types, except all `*/deleted` events, the `resource` attribute of the event contains the full data for the corresponding API resource **after** the event. The `resource` object contains keys corresponding to the attributes that the Integration API resource has, including it's `id`. For list of those attributes, see the Integration API reference for each resource type (e.g. [user](https://www.sharetribe.com/api-reference/integration.html#users), [listing](https://www.sharetribe.com/api-reference/integration.html#users), etc). In addition, when the resource has one or more 1-to-1 [relationships](https://www.sharetribe.com/api-reference/#including-related-resources) with other Integration API resources (for instance, [listing author](https://www.sharetribe.com/api-reference/integration.html#listing-relationships) or [user profileImage](https://www.sharetribe.com/api-reference/integration.html#user-relationships)), the `resource` object also contains keys with the same names as these relationships. The corresponding values are objects with single key `id` and value the ID of the related resource. However, generally 1-to-many relationships are not included in the resource data, with one notable exception: the `images` relationship of `listing` resources is always included. Note that the event data is immutable. This means that if a new attribute or relationship is added to some Integration API resource, older events for that resource type will not include data about that attribute or relationship. The `previousValues` attribute holds an object describing the attributes and/or relationships of the resource prior to the event, subject to the following rules: - Only the attributes or relationships affected in the event are included, i.e. the `previousValues` holds a subset of a resource data that describe the _difference_ between the resource before and after the event. - Resource attribute and relationship values are given in their entirety, including when the value is some nested object or array (such as the listing's `availabilityPlan` or transaction's `lineItems`, for instance). - If an attribute that did not have a value is updated, it will be present with `null` value in the `previousValues`. - Extended data attributes (such as user profile's `publicData`, listing `metadata`, etc) are treated as collection of key-value pairs and the difference computation treats each top-level extended data key separately: - If a key that did not exist in the extended data was added, the key will be given with `null` value in the `previousValues` object. - If a key had different value or was removed, the entire previous value is given in the `previousValues` object, including when the value is a nested data structure. Note that for all `*/deleted` events, some of the resources' attributes may occasionally have `null` values in the `previousValues` object, due to stricter data deletion requirements. ## Event sources The following table lists all possible event sources: | Source | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `source/marketplace-api` | The event happened through the Marketplace API. | | `source/integration-api` | The event happened through the Integration API. | | `source/transaction` | The event happened as part of a transaction transition, regardless of whether the transition was invoked via some API call or via Console. | | `source/console` | The event happened through Sharetribe Console. | | `source/admin` | The event happened as a result of a Sharetribe team member action (product support). | ## Supported event types The currently supported event types and their corresponding Integration API resource types are: | Event type | Integration API resource | Description | | ------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `listing/created` | [listing](https://www.sharetribe.com/api-reference/integration.html#listings) | A new listing was created. | | `listing/updated` | [listing](https://www.sharetribe.com/api-reference/integration.html#listings) | An existing listing was updated, including when the set of listing images is updated. | | `listing/deleted` | [listing](https://www.sharetribe.com/api-reference/integration.html#listings) | A listing was deleted. | | `user/created` | [user](https://www.sharetribe.com/api-reference/integration.html#users) | A new Sharetribe marketplace user was created. | | `user/updated` | [user](https://www.sharetribe.com/api-reference/integration.html#users) | An existing user was updated. | | `user/deleted` | [user](https://www.sharetribe.com/api-reference/integration.html#users) | A user was deleted. | | `availabilityException/created` | [availabilityException](https://www.sharetribe.com/api-reference/integration.html#availability-exceptions) | A new availability exception was created for a listing. | | `availabilityException/updated` | [availabilityException](https://www.sharetribe.com/api-reference/integration.html#availability-exceptions) | An existing availability exception was updated. | | `availabilityException/deleted` | [availabilityException](https://www.sharetribe.com/api-reference/integration.html#availability-exceptions) | An availability exception was deleted. | | `message/created` | [message](https://www.sharetribe.com/api-reference/integration.html#messages) | A new message was sent for a transaction. | | `message/updated` | [message](https://www.sharetribe.com/api-reference/integration.html#messages) | An existing message was updated. | | `message/deleted` | [message](https://www.sharetribe.com/api-reference/integration.html#messages) | A message was deleted. | | `transaction/initiated` | [transaction](https://www.sharetribe.com/api-reference/integration.html#transactions) | A new transaction was initiated. | | `transaction/transitioned` | [transaction](https://www.sharetribe.com/api-reference/integration.html#transactions) | An existing transaction transitioned to a new state. | | `transaction/updated` | [transaction](https://www.sharetribe.com/api-reference/integration.html#transactions) | An existing transaction was updated without a transition (e.g. via API call to [update metadata](https://www.sharetribe.com/api-reference/integration.html#update-transaction-metadata)). | | `transaction/deleted` | [transaction](https://www.sharetribe.com/api-reference/integration.html#transactions) | An existing transaction was deleted. | | `booking/created` | [booking](https://www.sharetribe.com/api-reference/integration.html#bookings) | A new booking was created. | | `booking/updated` | [booking](https://www.sharetribe.com/api-reference/integration.html#bookings) | An existing booking was updated. | | `booking/deleted` | [booking](https://www.sharetribe.com/api-reference/integration.html#bookings) | A booking was deleted. | | `review/created` | [review](https://www.sharetribe.com/api-reference/integration.html#reviews) | A new review was posted. | | `review/updated` | [review](https://www.sharetribe.com/api-reference/integration.html#reviews) | An existing review was updated. | | `review/deleted` | [review](https://www.sharetribe.com/api-reference/integration.html#reviews) | A review was deleted. | | `stockAdjustment/created` | [stockAdjustment](https://www.sharetribe.com/api-reference/integration.html#stock-adjustments) | A new stock adjustment was created. | | `stockAdjustment/updated` | [stockAdjustment](https://www.sharetribe.com/api-reference/integration.html#stock-adjustments) | An existing stock adjustment was updated. | | `stockAdjustment/deleted` | [stockAdjustment](https://www.sharetribe.com/api-reference/integration.html#stock-adjustments) | A stock adjustment was deleted. | | `stockReservation/created` | [stockReservation](https://www.sharetribe.com/api-reference/integration.html#stock-reservations) | A new stock reservation was created. | | `stockReservation/updated` | [stockReservation](https://www.sharetribe.com/api-reference/integration.html#stock-reservations) | An existing stock reservation was updated. | | `stockReservation/deleted` | [stockReservation](https://www.sharetribe.com/api-reference/integration.html#stock-reservations) | A stock reservation was deleted. | The event type follows the format `RESOURCE_TYPE/EVENT_SUBTYPE`. New event types may be added at any moment. Make sure your integration handles event types not given in this list gracefully (by ignoring them, for instance). Note that some event types can occur even though there is currently no support for the corresponding functionality in the Sharetribe APIs or Sharetribe Console. Typically this can happen when the event was caused internally by an administrative action of the Sharetribe team, in which case the `source` of the event would be `source/admin`. ## Transaction process actions and booking, stock reservation and review events In Sharetribe, bookings, stock reservations and reviews are primarily managed through the transaction process using the [booking-](/references/transaction-process-actions/#bookings), [stock reservations-](/references/transaction-process-actions/#stock-reservations) and [review-related](/references/transaction-process-actions/#reviews) actions. Sharetribe emits events separately when each of these actions takes effect, even if multiple actions occur within the same transaction transition. For instance, if a transition includes both `:action/create-pending-booking` and `:action/accept-booking`, Sharetribe generates at least three events as a result. First, there is a `:booking/created` event, followed by a `:booking/updated` event reflecting the state change of the booking and finally a `:transaction/initialized` or a `:transaction/transitioned` event for the transaction itself. Note that this does not apply to the `:transaction/*` events and the actions that manipulate the transaction data itself (such as `:action/privileged-set-line-items`). For the transaction resource itself, there is a single event emitted that corresponds to the entire transition. The corresponding `previousValues` for the transaction will reflect data before the transition. ## Further reading - [Integration API reference for events](https://www.sharetribe.com/api-reference/integration.html#events) - [Using Sharetribe CLI to view event data](/how-to/events/view-events-with-sharetribe-cli/) - [Reacting to events](/how-to/events/reacting-to-events/) how-to guide - A [full example](https://github.com/sharetribe/integration-api-examples/blob/master/scripts/notify-new-listings.js) Integration API application is available [in the Integration API examples](https://github.com/sharetribe/integration-api-examples/) repository --- ## Extended data Path: references/extended-data/index.mdx # Extended data _Extended data_ is a set of arbitrary keys and values stored with the API resources. The values for the keys can be any valid JSON values, including a JSON object (hash). This provides API clients with the capability to store arbitrary structured data for the supported resource types. Via search schema we also support querying, filtering, and sorting by extended data for some value types. For all types of extended data across all resources, the total size of an extended data object as JSON string must not exceed 50KB. ## Types of extended data There are four types of extended data: _metadata_, _private data_, _protected data_ and _public data_. Each type has different access semantics (i.e. who is allowed to read or write the data). Extended data support in API resources is work in progress. See the [API reference](/concepts/api-sdk/api/) for each resource for information on supported extended data. ### Public data _Public data_ is writable by whoever has write access to a given resource (e.g. listing's author can write public data about the listing, a user can write public data in their profile). Public data is readable via all API endpoints that return the resource and by marketplace operators. Public data can be used, for instance, to store additional information about listings and users in order to help customers make buying decisions. ### Protected data _Protected data_ is writable and readable by whoever has write access to a resource. It is also readable by marketplace operators. Protected data can be revealed to all transaction parties via the marketplace [transaction process](/concepts/transactions/transaction-process/). Protected data can be used to store information that is only made visible to the transaction participants at a specific point in the [transaction process](/concepts/transactions/transaction-process/). ### Private data _Private data_ is similar to protected data, but is not intended to be revealed via the transaction process. For instance, it can be used to collect and store information about users or listings for marketplace operators. ### Metadata _Metadata_ is writable by marketplace operators and can be read via all API endpoints returning the corresponding resource. Metadata is supported for users and listings. It can also be used as filters when searching via `/listings/query` in Marketplace API, `/listings/query` in Integration API or `/users/query` in Integration API. Metadata can be used to store data about listings and users that must only be writable by marketplace operators or through integrations to other systems built around the Integration API and that the users themselves must not be able to modify. ## Search schema Extended data is available out of the box and can be written and read via the Marketplace API, Integration API and Console without any prior configuration. _Search schema_ may optionally be defined for some value types of extended data. When a schema is provided for a given extended data key, the API can use this information to make querying the extended data possible via some API endpoints. For instance `/listings/query` supports querying listings by public data or metadata. Search schemas can be managed with [Sharetribe CLI](/introduction/getting-started-with-sharetribe-cli/). With the CLI, you can list, set and unset search schemas for the following kinds of extended data: - listing's public data and metadata - user profile's metadata, private, protected and public data - transaction's protected data and metadata The commands to manage search schemas are: - `flex-cli search` List all defined data schemas - `flex-cli search set` Set (create or update) data schema - `flex-cli search unset` Unset data schema You can also check out our guide for [managing search schemas with Sharetribe CLI](/how-to/search/manage-search-schemas-with-sharetribe-cli/). Only top-level values in extended data can have a schema. --- ## References Path: references/index.mdx # References Technical reference documentation for Sharetribe. API specifications, configuration options, and data model references for extending and customizing your marketplace with code. --- ## Page asset schema Path: references/page-asset-schema/index.mdx # Page asset schema ## What is a page asset schema Page [assets](/references/assets/) in Sharetribe have a structure that is defined by the page asset schema. The Sharetribe page asset schema is based on [JSON schema](https://json-schema.org/). If you are not familiar with the JSON schema, you can learn more in the [Understanding JSON schema](https://json-schema.org/understanding-json-schema/) ebook. It is good to note that the content page asset schema is not the same as a [website structured data schema](https://schema.org/): - Sharetribe page asset schema is a Sharetribe-specific description of the data being returned from [Sharetribe Asset Delivery API](https://www.sharetribe.com/api-reference/asset-delivery-api.html) - Schema.org structured data schema is a general vocabulary for representing the data content of a page in a way that is easily readable by e.g. search engines. Page asset schema is also distinct from an [extended data search schema](/references/extended-data/#search-schema): - Page asset schema relates to page assets created by operators through Sharetribe Console. It enables building client applications that can predictably handle page asset data. - Extended data search schema relates to listings or users. It enables searching and filtering users and listings through the Sharetribe APIs on the marketplace. The page asset schema determines the structure of the page in both Sharetribe Console and the page asset fetched from Asset Delivery API. - The page is created and modified in Sharetribe Console, structured by the page asset schema - The page asset is then fetched to the client, and the data structure for all pages can be predicted based on the page asset schema. ![Page asset schema in context](./page-schema-context.png) ## Page asset schema syntax: properties and `$defs` The page asset schema itself has two main parts: - properties - `$defs` Properties describe the main structure of the page asset, whereas `$defs` contain subschemas. Subschemas are additional attributes that can be reused in several properties. - Read more about [defs in JSON schema](https://json-schema.org/understanding-json-schema/structuring.html#defs) For instance, a page section's call to action has an optional internal button link. In the schema, the _fieldType_ attribute determines whether the section has a call to action, and whether it is an internal link or an external link. The schema then determines that if _fieldType_ is required and is defined as internal button link, the relevant additional attributes are added to _callToAction_ . ```json { "callToAction": { "type": "object", "properties": { "fieldType": { "title": "Section call to action", "type": "string", "default": "none", "description": "The action the user is prompted to take after viewing the section.", "oneOf": [ { "$ref": "#/$defs/fieldType/none", "title": "No call to action" }, { "$ref": "#/$defs/fieldType/internalButtonLink", "title": "Internal link", "description": "Link to a page in your marketplace. Displayed as a button." }, { "$ref": "#/$defs/fieldType/externalButtonLink", "title": "External link", "description": "Link to a page outside your marketplace. Opens in a new tab. Displayed as a button." } ] } }, "allOf": [ { "if": { "properties": { "fieldType": { "$ref": "#/$defs/fieldType/internalButtonLink" } }, "required": ["fieldType"] }, "then": { "$ref": "#/$defs/internalButtonLink" } } ] } } ``` The definitions for `fieldType/internalButtonLink` and `internalButtonLink` can be found in `$defs`. ```json { "$defs": { "fieldType": { "none": { "const": "none" }, "internalButtonLink": { "const": "internalButtonLink" } }, "internalButtonLink": { "properties": { "content": { "title": "Internal link text", "type": "string", "minLength": 1 }, "href": { "description": "Include only the path after your domain. For example, if you want to link to your About page, use /p/about.", "title": "Internal link address", "type": "string", "examples": [ "#section-id-as-anchor", "/absolute/path/to/page" ], "pattern": "^(?![a-zA-Z][a-zA-Z\\+\\.\\-]*\\:)", "minLength": 1 } }, "required": [ "content", "href" ] } } ``` For a page asset section where `callToAction.fieldType` is internal button link, `callToAction` will also have the properties defined in `internalButtonLink`. ![Call to action with internal button](./internal-button-cta.png) ## Using page asset schema when building a client Since the page asset schema defines the structure of the page asset, any client applications need to be built so that they can handle all situations where the data is valid according to the schema. For instance, required attributes are explicitly defined in the schema, so all other attributes should be considered optional. For instance: - `sectionId` is required, and has a minimum length of 1. Client apps can expect all sections to have a `sectionId` string of 1 or more characters. - `section.title` is not required. Client apps can expect `section.title` to be an object, or `null`. - `section.title.content` is not required. Clients can expect `section.title.content` to be a string with 1 or more characters, or an empty string, or `null`. The schema is subject to change, in the sense that new properties may be introduced as time goes on, so client implementations should be able to handle those situations. ## Full page asset schema You can download the full page asset schema here: - [page-asset-schema.json](/resources/page-asset-schema.json) The representation below shows the page asset schema without `_editor` attributes, which are only used in Sharetribe Console internally and are subject to change. ```json { "type": "object", "properties": { "meta": { "title": "SEO & Social", "type": "object", "description": "Tell search engines and social media platforms how they should present your page in search results and posts.\n\n[Watch a video](https://www.youtube.com/watch?v=W7CocGvm7RI) | [Learn more about editing meta tags](https://www.sharetribe.com/help/en/articles/8411140-how-to-edit-seo-and-social-metadata-tags)", "properties": { "pageTitle": { "type": "object", "properties": { "fieldType": { "const": "metaTitle", "default": "metaTitle" }, "content": { "type": "string", "maxLength": 255, "title": "Page title", "description": "The page title in search engines and browser tabs. Recommended length: 50–60 characters." } } }, "pageDescription": { "type": "object", "properties": { "fieldType": { "default": "metaDescription", "const": "metaDescription" }, "content": { "type": "string", "title": "Page description", "description": "A summary of the page content for search engines. Recommended length: 50-160 characters." } } }, "socialSharing": { "type": "object", "properties": { "fieldType": { "const": "openGraphData", "default": "openGraphData" }, "title": { "type": "string", "title": "Page title for social media", "description": "The page title in social media shares and links. Recommended length: 50–60 characters." }, "description": { "type": "string", "title": "Page description for social media", "description": "A summary of the page content for social media shares and links. Recommended length: 50-160 characters." }, "image": { "title": "Page image for social media", "type": "object", "description": "The page image in social media shares and links. Recommended aspect ratio: 1.91:1. Recommended minimum size: 1200x630 pixels. Maximum image size: 20MB.", "properties": { "_ref": { "type": "object", "properties": { "resolver": { "const": "image" }, "target": { "type": "string" }, "params": { "const": { "variants": { "social600": { "width": 600, "height": 600, "fit": "scale" }, "social1200": { "width": 1200, "height": 1200, "fit": "scale" } } } } } } } } } } } }, "sections": { "title": "Sections", "description": "Build a content page out of sections and content blocks. Determine the section layout, color, and appearance. Add titles, descriptions, call-to-action buttons, block text, images, and video.\n\n[Watch a video](https://www.youtube.com/watch?v=nZ8YtfZ_5n0&t=124s) | [Learn more about editing content pages](https://www.sharetribe.com/help/en/articles/8387209-how-to-edit-a-content-page)", "type": "array", "maxItems": 20, "items": { "title": "Section details", "type": "object", "properties": { "sectionName": { "type": "string", "title": "Section name", "description": "The section name is only shown in Console. It helps you remember what the section is about." }, "sectionId": { "_errors": { "pattern": "You've added characters that are not allowed." }, "description": "Use an anchor link ID to link directly to a section. Example: www.example.com/p/page_id#anchor-link-id. Use lowercase characters, numbers, dashes (-) or underscores (_), and no spaces.", "type": "string", "title": "Anchor link ID", "pattern": "(^$)|^[a-z][a-z0-9_\\-]*$" }, "sectionType": { "description": "Determines the section layout. [Learn more about section templates.](https://www.sharetribe.com/help/en/articles/8387253-what-are-section-templates)", "type": "string", "title": "Section template", "oneOf": [ { "const": "hero", "title": "Hero", "description": "No content blocks. Consists of a title, description, and button." }, { "const": "article", "title": "Article", "description": "Content blocks stacked vertically, optimized for reading." }, { "const": "carousel", "title": "Carousel", "description": "Content blocks placed horizontally. 1-4 blocks are visible at a time and the rest can be revealed by swiping or scrolling." }, { "const": "columns", "title": "Columns", "description": "Content blocks in a grid of 1, 2, 3, or 4 columns." }, { "const": "features", "title": "Features", "description": "Content blocks stacked vertically, with text and media side by side in an alternating order." } ] }, "title": { "type": "object", "properties": { "content": { "title": "Section title", "type": "string" }, "fieldType": { "title": "Section title size", "type": "string", "default": "heading2", "oneOf": [ { "$ref": "#/$defs/fieldType/heading1", "title": "Page title (H1)" }, { "$ref": "#/$defs/fieldType/heading2", "title": "Section title (H2)" } ] } } }, "description": { "type": "object", "properties": { "fieldType": { "title": "Section description", "type": "string", "default": "paragraph", "$ref": "#/$defs/fieldType/paragraph" }, "content": { "title": "Section description", "type": "string" } } }, "callToAction": { "type": "object", "properties": { "fieldType": { "title": "Section call to action", "type": "string", "default": "none", "description": "The action you want a user to take after viewing the section.", "oneOf": [ { "$ref": "#/$defs/fieldType/none", "title": "No call to action" }, { "$ref": "#/$defs/fieldType/internalButtonLink", "title": "Internal link", "description": "A button link to a page in your marketplace." }, { "$ref": "#/$defs/fieldType/externalButtonLink", "title": "External link", "description": "A button link to a page outside your marketplace. Opens in a new tab." } ] } }, "allOf": [ { "if": { "properties": { "fieldType": { "$ref": "#/$defs/fieldType/internalButtonLink" } }, "required": ["fieldType"] }, "then": { "$ref": "#/$defs/internalButtonLink" } }, { "if": { "properties": { "fieldType": { "$ref": "#/$defs/fieldType/externalButtonLink" } }, "required": ["fieldType"] }, "then": { "$ref": "#/$defs/externalButtonLink" } } ] }, "appearance": { "type": "object", "properties": { "fieldType": { "title": "Section appearance", "type": "string", "default": "defaultAppearance", "oneOf": [ { "$ref": "#/$defs/fieldType/defaultAppearance", "title": "Default" }, { "$ref": "#/$defs/fieldType/customAppearance", "title": "Custom", "description": "Customize background color, background image, and text color." } ] } }, "if": { "properties": { "fieldType": { "const": "customAppearance" } }, "required": ["fieldType"] }, "then": { "properties": { "backgroundColor": { "title": "Background color", "description": "Displayed if the section doesn't have a background image.", "type": "string", "pattern": "^#[A-Fa-f0-9]{6}" }, "backgroundImage": { "title": "Background image", "type": "object", "description": "Minimum image dimensions for the best results: 1600x1200px. Maximum image size: 20MB.", "properties": { "_ref": { "type": "object", "properties": { "resolver": { "const": "image" }, "target": { "type": "string" }, "params": { "$ref": "#/$defs/imageParams/scaled" } } } } }, "backgroundImageOverlay": { "type": "object", "properties": { "preset": { "title": "Background image overlay", "description": "You can make the image darker to make the text easier to read.", "type": "string", "default": "none", "oneOf": [ { "const": "none", "title": "No overlay" }, { "const": "dark", "title": "Dark overlay" }, { "const": "darker", "title": "Darker overlay" } ] } }, "allOf": [ { "if": { "properties": { "preset": { "const": "dark" } }, "required": ["preset"] }, "then": { "properties": { "color": { "type": "string", "const": "#000000" }, "opacity": { "type": "number", "const": 0.3 } }, "required": ["color", "opacity"] } }, { "if": { "properties": { "preset": { "const": "darker" } }, "required": ["preset"] }, "then": { "properties": { "color": { "type": "string", "const": "#000000" }, "opacity": { "type": "number", "const": 0.5 } }, "required": ["color", "opacity"] } } ], "required": ["preset"] }, "textColor": { "title": "Text color", "type": "string", "default": "black", "oneOf": [ { "const": "black", "title": "Black" }, { "const": "white", "title": "White" } ] } }, "required": ["backgroundImageOverlay"] } } }, "allOf": [ { "if": { "properties": { "sectionType": { "enum": ["columns", "carousel", "article", "features"] } }, "required": ["sectionType"] }, "then": { "properties": { "blocks": { "title": "Content Blocks", "type": "array", "maxItems": 20, "items": { "type": "object", "properties": { "blockName": { "type": "string", "title": "Block name", "description": "The block name is only shown in Console. It helps you remember what the block is about." }, "blockId": { "_errors": { "pattern": "You've added characters that are not allowed." }, "description": "Use an anchor link ID to link directly to a block. Example: www.example.com/p/page_id#anchor-link-id. Use lowercase characters, numbers, dashes (-) or underscores (_), and no spaces.", "type": "string", "title": "Anchor link ID", "pattern": "(^$)|^[a-z][a-z0-9_\\-]*$" }, "blockType": { "title": "Block type", "const": "defaultBlock", "$comment": "Currently, we only have one block type but in the future there could be many" }, "media": { "type": "object", "properties": { "fieldType": { "title": "Block media", "type": "string", "default": "none", "oneOf": [ { "$ref": "#/$defs/fieldType/none", "title": "No media" }, { "$ref": "#/$defs/fieldType/image", "title": "Image" }, { "$ref": "#/$defs/fieldType/youtube", "title": "YouTube video" } ] } }, "allOf": [ { "if": { "properties": { "fieldType": { "$ref": "#/$defs/fieldType/image" } } }, "then": { "properties": { "image": { "title": "Image file", "type": "object", "description": "Sharetribe supports most common image formats. Maximum image size: 20MB.", "properties": { "_ref": { "type": "object", "properties": { "resolver": { "const": "image" }, "target": { "type": "string" } }, "required": ["resolver", "target"] } }, "required": ["_ref"] }, "aspectRatio": { "$ref": "#/$defs/aspectRatio" }, "alt": { "description": "A short description of the image for accessibility and search engines.", "title": "Image alt text", "type": "string", "minLength": 1 }, "link": { "__features": [ "clickable-block-image", { "type": "object", "properties": { "fieldType": { "title": "Block image link", "type": "string", "default": "none", "oneOf": [ { "$ref": "#/$defs/fieldType/none", "title": "No link" }, { "$ref": "#/$defs/fieldType/internalImageLink", "title": "Internal link", "description": "A link to a page in your marketplace." }, { "$ref": "#/$defs/fieldType/externalImageLink", "title": "External link", "description": "A link to a page outside your marketplace. Opens in a new tab." } ] } }, "allOf": [ { "if": { "properties": { "fieldType": { "$ref": "#/$defs/fieldType/internalImageLink" } }, "required": ["fieldType"] }, "then": { "$ref": "#/$defs/internalImageLink" } }, { "if": { "properties": { "fieldType": { "$ref": "#/$defs/fieldType/externalImageLink" } }, "required": ["fieldType"] }, "then": { "$ref": "#/$defs/externalImageLink" } } ] } ] } }, "required": [ "alt", "image", "aspectRatio" ], "allOf": [ { "if": { "properties": { "aspectRatio": { "const": "1/1" } }, "required": ["aspectRatio"] }, "then": { "properties": { "image": { "properties": { "_ref": { "properties": { "params": { "type": "object", "$ref": "#/$defs/imageParams/square" } } } } } } } }, { "if": { "properties": { "aspectRatio": { "const": "16/9" } }, "required": ["aspectRatio"] }, "then": { "properties": { "image": { "properties": { "_ref": { "properties": { "params": { "type": "object", "$ref": "#/$defs/imageParams/landscape" } } } } } } } }, { "if": { "properties": { "aspectRatio": { "const": "2/3" } }, "required": ["aspectRatio"] }, "then": { "properties": { "image": { "properties": { "_ref": { "properties": { "params": { "type": "object", "$ref": "#/$defs/imageParams/portrait" } } } } } } } }, { "if": { "properties": { "aspectRatio": { "const": "auto" } }, "required": ["aspectRatio"] }, "then": { "properties": { "image": { "properties": { "_ref": { "properties": { "params": { "type": "object", "$ref": "#/$defs/imageParams/original" } } } } } } } } ] } }, { "if": { "properties": { "fieldType": { "$ref": "#/$defs/fieldType/youtube" } } }, "then": { "properties": { "youtubeVideoId": { "_errors": { "pattern": "YouTube video ID must contain only letters and numbers and _ or - characters." }, "description": "The part of a YouTube link after \"watch?v=\". For example, for the video youtube.com/watch?v=UffchBUUIoI, the ID is UffchBUUIoI.", "type": "string", "title": "YouTube video ID", "pattern": "^[0-9A-Za-z_-]+$" }, "aspectRatio": { "$ref": "#/$defs/aspectRatio" } }, "required": [ "youtubeVideoId", "aspectRatio" ] } } ] }, "title": { "type": "object", "properties": { "content": { "title": "Block title", "type": "string" }, "fieldType": { "type": "string", "title": "Block title size", "default": "heading3", "oneOf": [ { "$ref": "#/$defs/fieldType/heading1", "title": "Page title (H1)" }, { "$ref": "#/$defs/fieldType/heading2", "title": "Section title (H2)" }, { "$ref": "#/$defs/fieldType/heading3", "title": "Section subtitle (H3)" } ] } } }, "text": { "type": "object", "properties": { "fieldType": { "type": "string", "title": "Text element type", "default": "markdown", "$ref": "#/$defs/fieldType/markdown" }, "content": { "title": "Block text", "type": "string", "description": "You can format text with markdown. [Learn more about markdown.](https://www.sharetribe.com/help/en/articles/8404687-how-to-format-your-text-in-pages-with-markdown)" } } }, "callToAction": { "type": "object", "properties": { "fieldType": { "title": "Block call to action", "type": "string", "default": "none", "description": "The action you want a user to take after viewing the block.", "oneOf": [ { "$ref": "#/$defs/fieldType/none", "title": "No call to action" }, { "$ref": "#/$defs/fieldType/internalButtonLink", "title": "Internal link", "description": "A button link to a page in your marketplace." }, { "$ref": "#/$defs/fieldType/externalButtonLink", "title": "External link", "description": "A button link to a page outside your marketplace. Opens in a new tab." } ] } }, "allOf": [ { "if": { "properties": { "fieldType": { "$ref": "#/$defs/fieldType/internalButtonLink" } }, "required": ["fieldType"] }, "then": { "$ref": "#/$defs/internalButtonLink" } }, { "if": { "properties": { "fieldType": { "$ref": "#/$defs/fieldType/externalButtonLink" } }, "required": ["fieldType"] }, "then": { "$ref": "#/$defs/externalButtonLink" } } ] }, "alignment": { "__features": [ "block-alignment", { "title": "Block content alignment", "type": "string", "default": "left", "oneOf": [ { "const": "left", "title": "Align content left" }, { "const": "center", "title": "Center content" }, { "const": "right", "title": "Align content right" } ] } ] } }, "required": ["blockType"] } } } } }, { "if": { "anyOf": [ { "properties": { "sectionType": { "const": "columns" } }, "required": ["sectionType"] }, { "properties": { "sectionType": { "const": "carousel" } }, "required": ["sectionType"] } ] }, "then": { "properties": { "numColumns": { "title": "Number of columns", "type": "integer", "oneOf": [ { "const": 1, "title": "1" }, { "const": 2, "title": "2" }, { "const": 3, "title": "3" }, { "const": 4, "title": "4" } ] } }, "required": ["numColumns"] } } ], "required": ["sectionType"] } } }, "$defs": { "fieldType": { "youtube": { "const": "youtube" }, "internalButtonLink": { "const": "internalButtonLink" }, "internalImageLink": { "const": "internalImageLink" }, "none": { "const": "none" }, "defaultAppearance": { "const": "defaultAppearance" }, "heading1": { "const": "heading1" }, "image": { "const": "image" }, "heading3": { "const": "heading3" }, "externalButtonLink": { "const": "externalButtonLink" }, "externalImageLink": { "const": "externalImageLink" }, "heading2": { "const": "heading2" }, "customAppearance": { "const": "customAppearance" }, "paragraph": { "const": "paragraph" }, "markdown": { "const": "markdown" } }, "internalButtonLink": { "properties": { "content": { "title": "Internal link text", "type": "string", "minLength": 1 }, "href": { "_errors": { "pattern": "This field should not include protocol like https." }, "description": "Include only the path after your domain. For example, if you want to link to your About page, use \"/p/about\", or if you want to link to your landing page, use \"/\".", "title": "Internal link address", "type": "string", "examples": [ "#section-id-as-anchor", "/absolute/path/to/page" ], "pattern": "^(?![a-zA-Z][a-zA-Z+.-]*:)", "minLength": 1 } }, "required": ["content", "href"] }, "externalButtonLink": { "properties": { "content": { "title": "External link text", "type": "string", "minLength": 1 }, "href": { "_errors": { "pattern": "The address doesn't start with https://." }, "description": "The external link address should begin with https://.", "title": "External link address", "type": "string", "examples": ["http:", "https:"], "pattern": "^(http|https):", "minLength": 1 } }, "required": ["content", "href"] }, "internalImageLink": { "properties": { "href": { "_errors": { "pattern": "This field should not include protocol like https." }, "description": "Include only the path after your domain. For example, if you want to link to your About page, use \"/p/about\", or if you want to link to your landing page, use \"/\".", "title": "Internal link address", "type": "string", "examples": [ "#section-id-as-anchor", "/absolute/path/to/page" ], "pattern": "^(?![a-zA-Z][a-zA-Z+.-]*:)", "minLength": 1 } }, "required": ["href"] }, "externalImageLink": { "properties": { "href": { "_errors": { "pattern": "The address doesn't start with https://." }, "description": "The external link address should begin with https://.", "title": "External link address", "type": "string", "examples": ["http:", "https:"], "pattern": "^(http|https):", "minLength": 1 } }, "required": ["href"] }, "imageParams": { "scaled": { "const": { "variants": { "scaled800": { "width": 800, "height": 800, "fit": "scale" }, "scaled1200": { "width": 1200, "height": 1200, "fit": "scale" }, "scaled2400": { "width": 2400, "height": 2400, "fit": "scale" } } } }, "square": { "const": { "variants": { "square400": { "width": 400, "height": 400, "fit": "crop" }, "square800": { "width": 800, "height": 800, "fit": "crop" }, "square1200": { "width": 1200, "height": 1200, "fit": "crop" }, "square2400": { "width": 2400, "height": 2400, "fit": "crop" } } } }, "landscape": { "const": { "variants": { "landscape400": { "width": 400, "height": 225, "fit": "crop" }, "landscape800": { "width": 800, "height": 450, "fit": "crop" }, "landscape1200": { "width": 1200, "height": 675, "fit": "crop" }, "landscape2400": { "width": 2400, "height": 1350, "fit": "crop" } } } }, "portrait": { "const": { "variants": { "portrait400": { "width": 400, "height": 600, "fit": "crop" }, "portrait800": { "width": 800, "height": 1200, "fit": "crop" }, "portrait1200": { "width": 1200, "height": 1800, "fit": "crop" }, "portrait2400": { "width": 2400, "height": 3600, "fit": "crop" } } } }, "original": { "const": { "variants": { "original400": { "width": 400, "height": 400, "fit": "scale" }, "original800": { "width": 800, "height": 800, "fit": "scale" }, "original1200": { "width": 1200, "height": 1200, "fit": "scale" }, "original2400": { "width": 2400, "height": 2400, "fit": "scale" } } } } }, "aspectRatio": { "title": "Aspect ratio", "type": "string", "default": "auto", "oneOf": [ { "const": "1/1", "title": "Square (1:1)" }, { "const": "16/9", "title": "Landscape (16:9)" }, { "const": "2/3", "title": "Portrait (2:3)" }, { "const": "auto", "title": "Original" } ] } } } ``` --- ## Listing stock management Description: Reference documentation for listing stock management. Path: references/stock/index.mdx # Listing stock management The listing stock management features of Sharetribe allow listing authors to define the quantity of available stock associated with the listing. There are three key concepts related to stock management: - **stock** represents the quantity of available units, associated with a listing. The units can be anything that makes sense for a given listing or marketplace concept, as long as the quantity is integer number. For instance - the number of physical items kept in inventory; a number of virtual items; number of available batches, when a listing is sold in batches rather than as individual items, etc. - A **stock adjustment** is a record of change in the quantity of stock for a listing. A positive adjustment represents an increase in available stock, while a negative adjustment represents a decrease. For instance, listing authors add stock by recording a positive stock adjustment, while sales that happen through are transaction are recorded as negative adjustments. Stock adjustments are immutable. - A **stock reservation** represents a reservation or purchase of particular quantity of stock through a transaction. Listing authors and operators can manage stock for listings through the Marketplace API and the Integration API. Stock adjustments can be created either [directly](https://www.sharetribe.com/api-reference/marketplace.html#create-stock-adjustment), or through a ["compare and set" operation that sets the total available stock](https://www.sharetribe.com/api-reference/marketplace.html#compare-and-set-total-stock) consistently, given an expected current total stock quantity for a listing. All stock reservations are created through transactions, governed by your [transaction process](/concepts/transactions/transaction-process/). An appropriately constructed transaction process ensures that reservations can only be placed for available quantities of stock. ## Stock reservation states A stock reservation can be in one of several possible states: `pending`, `proposed`, `accepted`, `cancelled` or `declined`. Just as with [bookings](/references/availability/#booking-states), stock reservations change state only through a corresponding transaction transition, using one of the [stock reservation actions](/references/transaction-process-actions/#stock-reservations). All stock reservations in `pending` or `accepted` states count as reservation against the listing's available stock. On the other hand, stock reservations in the `proposed`, `cancelled` or `declined` states do not affect the current available stock. It is important to note that the stock reservation's effect on stock is explicitly recorded as individual stock adjustments as the reservation progresses through its states. For example, if a proposed reservation is accepted, a new stock adjustment with negative sign (decrease of stock) is recorded. Similarly, if an `accepted` stock reservation changes state to `cancelled`, a positive stock adjustment (increase of stock) is recorded. On the other hand, a new stock adjustment is not recorded when the change of states does not require one, as is the case for change from `pending` to `accepted` state. All this ensures that the set of stock adjustments for a particular listing represents an accurate and detailed view of how the listing's available stock has changed over time. The figure below illustrates the possible stock reservation states, transitions between the states and the corresponding actions that you can use in your transaction process. ![Stock reservation states](./stock-reservation-states.png) ## Negative stock quantity Generally, the total available stock of a listing cannot be a negative number. However, in some rare cases, it is possible that the total stock becomes negative. This should be considered an error case and is an indication that at least some [transaction transition](/concepts/transactions/transaction-process/) failed to execute properly. Such a transaction transition would include some of the actions that _release_ some reserved stock (such as `:action/decline-stock-reservation` or `:action/cancel-stock-reservation`), which would typically be present in a transition representing a cancellation or a refund. As a mitigation, **it is recommended to always place actions that release a stock reservation last in the sequence of actions of a transition**. ## Further reading - Marketplace API reference - [stock](https://www.sharetribe.com/api-reference/marketplace.html#stock) - [stockAdjustments](https://www.sharetribe.com/api-reference/marketplace.html#stock-adjustments) - [stockReservations](https://www.sharetribe.com/api-reference/marketplace.html#stock-reservations) - Integration API reference - [stock](https://www.sharetribe.com/api-reference/integration.html#stock) - [stockAdjustments](https://www.sharetribe.com/api-reference/integration.html#stock-adjustments) - [stockReservations](https://www.sharetribe.com/api-reference/integration.html#stock-reservations) - [Transaction process actions for stock reservations](/references/transaction-process-actions/#stock-reservations) - [Example transaction process definition](https://github.com/sharetribe/example-processes#default-purchase) for selling products using stock --- ## Transaction process actions Path: references/transaction-process-actions/index.mdx # Transaction process actions A transaction process contains transitions and each transition defines an ordered list of 0 or more actions. Actions are instructions for the transaction engine and define what happens when a transition is executed. The ordering of the actions matters because they are executed in the given order. This is a reference for all the available actions with their preconditions, parameters, and configuration options. With **parameters** we mean values that are sent in the API calls to the actions, and with **configuration options** we mean the options that are set for the actions in the `process.edn` file. To learn more, see the [Transaction process format](/references/transaction-process-format/) reference article. ## Actions ### Transaction initialization #### `:action.initializer/init-listing-tx` This action is implicit and must not be in the process description. Initialize a new transaction from a listing using a standard transaction flow: customer initializes the transaction and provider is the author of the transaction listing. **Preconditions**: - Listing with the given ID must exist - Listing must have an author - Customer (actor of the transaction) must be a valid user - Author and customer must not be the same user **Parameters**: - `listingId`, UUID, mandatory **Configuration options**: - #### :action.initializer/init-reverse-listing-tx This action is implicit and must not be in the process description. Initialize a new reverse transaction from a listing using a reverse transaction flow: provider initializes the transaction and customer is the author of the transaction listing. **Preconditions**: - Listing with the given ID must exist - Listing must have an author - Provider (actor of the transaction) must be a valid user - Author and provider must not be the same user **Parameters**: - `listingId`, UUID, mandatory **Configuration options**: - ### Pricing #### `:action/privileged-set-line-items` Defines transaction price and breakdown. Sets given line items and calculates totals for each line item and for the entire transaction. Existing line items will be removed. If this action is run after a payment intent has already been created with `:action/stripe-create-payment-intent` or `:action/stripe-create-payment-intent-push`, the payin and payout information for the payment intent DO NOT get updated. This can result in payouts that do not correspond to the latest line item calculation. In other words, when used with the default Stripe integration, this action should only ever be used before creating the payment intent. **Preconditions**: - **Parameters**: - `lineItems`: Collection of line items (max 50). Each line items has following fields: - `code`: string, mandatory, identifies line item type (e.g. `"line-item/cleaning-fee"`), maximum length 64 characters. - `unitPrice`: money, mandatory - `lineTotal`: money - `quantity`: number - `percentage`: number (e.g. 15.5 for 15.5%) - `seats`: number - `units`: number - `includeFor`: array containing strings `"customer"` or `"provider"`, default `["customer", ":provider"]` Line item must have either `quantity` or `percentage` or both `seats` and `units`. `lineTotal` is calculated by the following rules: - If `quantity` is provided, the line total will be `unitPrice * quantity`. - If `percentage` is provided, the line total will be `unitPrice * (percentage / 100)`. - If `seats` and `units` are provided the line item will contain `quantity` as a product of `seats` and `units` and the line total will be `unitPrice * units * seats`. `lineTotal` can be optionally passed in. Will be validated against calculated line total. `includeFor` defines commissions. Customer commission is added by defining `includeFor` array `["customer"]` and provider commission by `["provider"]`. `payinTotal` is calculated by the action and added to the transaction. `payinTotal` equals to the sum of customer commission line totals and other non-commission line totals. Must be zero or positive. *Note:* Some line-item configurations are not supported by the default Stripe payment actions. If you use Stripe payment actions, `payinTotal` needs to be greater than zero, and greater than or equal to `payoutTotal`. For more details, refer to Stripe action documentation. `payoutTotal` is calculated by the action and added to the transaction. `payoutTotal` equals to the sum of provider commission line totals and other non-commission line totals. Must be zero or positive. Only one currency is allowed across all fields defining money. **Configuration options**: - #### `:action/calculate-tx-customer-commission` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items Calculates the customer commission, sets the transaction payin amount and adds a `:line-item/customer-commission` line-item. Subsequent executions will create new line items instead of modifying the existing one. Commission calculations are based on transaction/totalPrice and will accumulate the payinTotal. For example, a total price of 100 EUR and two consecutive 10 % customer commissions will result in an 120 EUR payin. **Preconditions**: - Transaction must have a total price - All currencies must match the listing currency - Min and max commissions need to be in correct order, i.e, min needs to be smaller than max **Parameters**: - **Configuration options**: - `commission`: decimal, mandatory. Acts as a multiplier on the transaction total, e.g. `0.1M` for a 10% commission. - `min`: a map with keys `:amount` and `:currency`, optional. Acts as a minimum commission, e.g. `{:amount 2M, :currency "EUR"}` for a 2EUR minimum. - `max`: a map with keys `:amount` and `:currency`, optional. Acts as a maximum commission, e.g. `{:amount 20M, :currency "EUR"}` for a 20EUR maximum. Where: - `amount`, decimal, mandatory. The value of a monetary unit. A decimal followed with a `M`, e.g. `10M`. - `currency`, string, mandatory, The three letter currency code of a monetary unit, e.g. `"EUR"`. #### `:action/calculate-tx-provider-commission` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items Calculates the provider commission, sets the transaction payout amount and adds a `:line-item/provider-commission` line-item. Subsequent executions will create new line items instead of modifying the existing one. Commission calculations are based on transaction/totalPrice and will accumulate the payoutTotal. For example, a total price of 100 EUR and two consecutive 10 % provider commissions will result in an 80 EUR payout. Too large commission (i.e. a negative payout) will cause an error. **Preconditions**: - Transaction must have a total price - All currencies must match the listing currency - Min and max commissions need to be in correct order, i.e, min needs to be smaller than max **Parameters**: - **Configuration options**: - `commission`: decimal, mandatory. Acts as a multiplier on the transaction total, e.g. `0.1M` for a 10% commission. - `min`: a map with mandatory keys `:amount` and `:currency`, optional. Acts as a minimum commission, e.g. `{:amount 2M, :currency "EUR"}` for a 2EUR minimum. - `max`: a map with keys `:amount` and `:currency`, optional. Acts as a maximum commission, e.g. `{:amount 20M, :currency "EUR"}` for a 20EUR minimum.. Where: - `amount`, decimal, mandatory. The value of a monetary unit. A decimal followed with a `M`, e.g. `10M`. - `currency`, string, mandatory, The three letter currency code of a monetary unit, e.g. `"EUR"`. #### `:action/calculate-tx-customer-fixed-commission` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items. Calculates a fixed commission for customer, sets the transaction pay out amount and adds a `:line-item/customer-fixed-commission` line-item. Subsequent executions will create new line items instead of modifying the existing one and will accumulate the `payinTotal`. For example, a total price of 100 EUR and two consecutive 10 EUR customer fixed commissions will result in an 120 EUR payin. **Preconditions**: - Transaction must have a total price - Commission currency must match the listing currency **Parameters**: - **Configuration options**: - `commission`: a map with mandatory keys `:amount` and `:currency`, mandatory. Acts as the fixed commission for a transaction, eg. `{:amount 10M :currency "EUR"}`. Where: - `amount`, decimal, mandatory. The value of a monetary unit. A decimal followed with a `M`, e.g. `10M`. - `currency`, string, mandatory, The three letter currency code of a monetary unit, e.g. `"EUR"`. #### `:action/calculate-tx-provider-fixed-commission` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items. Calculates a fixed commission for provider, sets the transaction pay out amount and adds a `:line-item/provider-fixed-commission` line-item. Subsequent executions will create new line items instead of modifying the existing one and will accumulate the `payoutTotal`. For example, a total price of 100 EUR and two consecutive 10 EUR provider fixed commissions will result in an 80 EUR payout. Too large commission (i.e. a negative payout) will cause an error. **Preconditions**: - Transaction must have a total price - Commission currency must match the listing currency **Parameters**: - **Configuration options**: - `commission`: a map with mandatory keys `:amount` and `:currency`, mandatory. Acts as the fixed commission for a transaction, eg. `{:amount 10M :currency "EUR"}`. Where: - `amount`, decimal, mandatory. The value of a monetary unit. A decimal followed with a `M`, e.g. `10M`. - `currency`, string, mandatory, The three letter currency code of a monetary unit, e.g. `"EUR"`. #### `:action/calculate-tx-nightly-total` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items. Calculates transaction total and provider commission from a nightly booking. #### `:action/calculate-tx-total` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items. Same as `:action/calculate-tx-nightly-total`, kept for backward compatibility. #### `:action/calculate-tx-daily-total` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items. Calculates transaction total and provider commission from a daily booking. #### `:action/calculate-tx-daily-total-price` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items Calculates transaction total from a daily booking. **Preconditions**: - Transaction must have a booking - Listing must have a price **Parameters**: - **Configuration options**: - #### `:action/calculate-tx-nightly-total-price` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items Calculates transaction total from a nightly booking. **Preconditions**: - Transaction must have a booking - Listing must have a price **Parameters**: - **Configuration options**: - #### `:action/calculate-tx-total-daily-booking-exclude-start` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items Calculates transaction total from daily booking without start and end dates. Can be used for price calculation of bookings where the boundary dates are used for delivery and pickup. **Preconditions**: - Transaction must have a booking - Listing must have a price - Booking time must be at least three days as there needs to be one day between the excluded dates **Parameters**: - **Configuration options**: - #### `:action/calculate-tx-two-units-total-price` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items. Calculates transaction total from the quantity params and the price multiplier configuration options. `total = quantity1 * quantity1-price-multiplier + quantity2 * quantity2-price-multiplier` **Preconditions**: - Listing must have a price **Parameters**: - `quantity1`: non-negative integer (0, 1, 2, ...), mandatory - `quantity2`: non-negative integer (0, 1, 2, ...), mandatory **Configuration options**: - `quantity1-price-multiplier`: decimal, defaults to `1.0M` - `quantity2-price-multiplier`: decimal, defaults to `1.0M` #### `:action/calculate-tx-unit-total-price` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items Calculates transaction total from given quantity and listing price. **Preconditions**: - Listing must have a price **Parameters**: - `quantity`: positive integer (1, 2, 3, ...), mandatory **Configuration options**: - #### `:action/calculate-full-refund` Calculates full refund for provider. Sets transaction pay in and pay out amounts to zero and creates reverse line items that undo all the previous line items. This action must not be run more than once. **Preconditions**: - **Parameters**: - **Configuration options**: - #### `:action/set-negotiated-total-price` Enables price negotiation. When the action is run for the first time it adds a new line item that makes the transaction total to match with the offer. Subsequent executions update the line item to match the counter offer. **Preconditions**: - **Parameters**: - `negotiatedTotal`, money, mandatory **Configuration options**: - #### `:action/set-line-items-and-total` **Deprecated**: use `:privileged-set-line-items` to have full control of the calculation and the line items. Enables custom pricing. Sets given line items and calculates totals for each line item and for the transaction. Existing line items will be removed. **Preconditions**: - **Parameters**: - `lineItems`: Collection of line items (max 50). Each line items has following fields: - `code`: string, mandatory, identifies line item type (e.g. `"cleaning-fee"`) - `unitPrice`: money, mandatory - `quantity`: number - `percentage`: number (e.g. 15.5 for 15.5%) - `seats`: number - `units`: number Line item must have either `quantity` or `percentage` or both `seats` and `units`. If `quantity` is provided, the line total will be `unitPrice * quantity`. If `percentage` is provided, the line total will be `unitPrice * (percentage / 100)`. If `seats` and `units` are provided the line item will contain `quantity` as a product of `seats` and `units` and the line total will be `unitPrice * units * seats`. **Configuration options**: - ### Bookings #### `:action/create-pending-booking` Creates a new booking in `pending` state with given start and end time, as long as the listing's availability for the given time range permits the booking to be created. Optionally takes booking display start and end times as well as seats. Bookings in `pending` state make a reservation against the listing's availability for the corresponding time slot with the given number of seats. **Preconditions**: The listing must have sufficient availability for the time slot between `bookingStart` and `bookingEnd` (exclusive). **Parameters**: - `bookingStart`: timestamp, mandatory. Used as the booking start time if the `type` is set to `:time` If the `type` is set to `:day`, the value is converted to UTC midnight and used as the booking start date. Marks the start of the timeslot that will be reserved in the listing's availability. Available in transaction process as `:time/booking-start` timepoint. - `bookingEnd`: timestamp, mandatory Used as the booking end time if the `type` is set to `:time` If the `type` is set to `:day`, the value is converted to UTC midnight and used as the booking end date. Please note that the `bookingEnd` is exclusive. Marks the end of the timeslot that will be reserved in the listing's availability. Available in the transaction process as `:time/booking-end` timepoint. - `bookingDisplayStart`: timestamp, optional. Moment of time that is displayed to a user as a booking start time. Does not affect availability of the listing. If not given, defaults to the value of `bookingStart` (normalized to UTC midnight, when `type` is `:day`). Available in the transaction process as `:time/booking-display-start` timepoint. - `bookingDisplayEnd`: timestamp, optional. Moment of time that is displayed to a user as a booking end time. Does not affect availability of the listing. If not given, defaults to the value of `bookingEnd` (normalized to UTC midnight, when `type` is `:day`). Available in the transaction process as `:time/booking-display-end` timepoint. - `seats`, integer, optional, defaults to 1 The number of seats that the booking reserves in the listing's availability. **Configuration options**: - `type`: enum, one of `:day`, `:time`. Defaults to `:day`. If set to `:day` normalizes `bookingStart` and `bookingEnd` values to midnight UTC. #### `:action/create-proposed-booking` Creates a new booking in `proposed` state with given start and end time, as long as the listing's availability for the given time range permits the booking to be created. Optionally takes booking display start and end times as well as seats. Bookings in `proposed` state do not affect the listing's availability, i.e. they do not reserve the time slot, until they are accepted using the `:action/accept-booking`. **Preconditions**: The listing must have sufficient availability for the time slot between `bookingStart` and `bookingEnd` (exclusive). **Parameters**: - `bookingStart`: timestamp, mandatory. Used as the booking start time if the `type` is set to `:time` If the `type` is set to `:day`, the value is converted to UTC midnight and used as the booking start date. Marks the start of the timeslot that will be reserved in the listing's availability, if the booking is subsequently accepted. Available in transaction process as `:time/booking-start` timepoint. - `bookingEnd`: timestamp, mandatory Used as the booking end time if the `type` is set to `:time` If the `type` is set to `:day`, the value is converted to UTC midnight and used as the booking end date. Please note that the `bookingEnd` is exclusive. Marks the end of the timeslot that will be reserved in the listing's availability, if the booking is subsequently accepted. Available in the transaction process as `:time/booking-end` timepoint. - `bookingDisplayStart`: timestamp, optional. Moment of time that is displayed to a user as a booking start time. Does not affect availability of the listing. If not given, defaults to the value of `bookingStart` (normalized to UTC midnight, when `type` is `:day`). Available in the transaction process as `:time/booking-display-start` timepoint. - `bookingDisplayEnd`: timestamp, optional. Moment of time that is displayed to a user as a booking end time. Does not affect availability of the listing. If not given, defaults to the value of `bookingEnd` (normalized to UTC midnight, when `type` is `:day`). Available in the transaction process as `:time/booking-display-end` timepoint. - `seats`, integer, optional, defaults to 1 The number of seats that the booking reserves in the listing's availability. **Configuration options**: - `type`: enum, one of `:day`, `:time`. Defaults to `:day`. If set to `:day` normalizes `bookingStart` and `bookingEnd` values to midnight UTC. #### `:action/create-booking` **DEPRECATED**: use `:action/create-pending-booking` or `:action/create-proposed-booking` instead. Creates a new booking in `pending` state with given start and end time. Optionally takes booking display start and end times as well as seats. **Preconditions**: - **Parameters**: - `bookingStart`: timestamp, mandatory. Used as the booking start time if the `type` is set to `:time` If the `type` is set to `:day`, the value is converted to UTC midnight and used as the booking start date. Marks the start of the timeslot that will be reserved in the listing's availability. Available in transaction process as `:time/booking-start` timepoint. - `bookingEnd`: timestamp, mandatory Used as the booking end time if the `type` is set to `:time` If the `type` is set to `:day`, the value is converted to UTC midnight and used as the booking end date. Please note that the `bookingEnd` is exclusive. Marks the end of the timeslot that will be reserved in the listing's availability. Available in the transaction process as `:time/booking-end` timepoint. - `bookingDisplayStart`: timestamp, optional. Moment of time that is displayed to a user as a booking start time. Does not affect availability of the listing. If not given, defaults to the value of `bookingStart` (normalized to UTC midnight, when `type` is `:day`). Available in the transaction process as `:time/booking-display-start` timepoint. - `bookingDisplayEnd`: timestamp, optional. Moment of time that is displayed to a user as a booking end time. Does not affect availability of the listing. If not given, defaults to the value of `bookingEnd` (normalized to UTC midnight, when `type` is `:day`). Available in the transaction process as `:time/booking-display-end` timepoint. - `seats`, integer, optional, defaults to 1 The number of seats that the booking reserves in the listing's availability. **Configuration options**: - `observe-availability?`: boolean, defaults to `false`. If set to `true`, prevents creating new bookings if the booking time is not available. - `type`: enum, one of `:day`, `:time`. Defaults to `:day`. If set to `:day` normalizes `bookingStart` and `bookingEnd` values to midnight UTC. #### `:action/accept-booking` Marks booking as accepted. Bookings in `accepted` state make a reservation against the listing's availability for the corresponding time slot and number of seats. **Preconditions**: - Transaction must have a booking - Booking must be in either `proposed` or `pending` state - If the booking is in proposed state, the listing must have sufficient availability for the time slot defined by the booking. **Parameters**: - **Configuration options**: - #### `:action/update-booking` Update the details of a booking - start and end times, start and end display times and seats. If one end of a time range is given, so must be the other. When updated, the booking remains in the same state it was before the update. Only the given attributes are updated and ones that are left out remain unchanged. In particular, the booking display times are NOT updated automatically when the booking start/end time is updated. **Preconditions**: - Transaction must have a booking - Booking must be in either `proposed`, `pending` or `accepted` state - The listing must have sufficient availability for the new (updated) time slot defined by the booking. **Parameters**: - `bookingStart`: timestamp, optional, must be given if `bookingEnd` is given Used as the booking start time if the booking `type` is `:time`. If the booking `type` is `:day`, the value is converted to UTC midnight and used as the booking start date. Marks the start of the timeslot that will be reserved in the listing's availability. Available in transaction process as `:time/booking-start` timepoint. - `bookingEnd`: timestamp, optional, must be given if `bookingStart` is given Used as the booking end time if the booking `type` is `:time`. If the booking `type` is `:day`, the value is converted to UTC midnight and used as the booking end date. Please note that the `bookingEnd` is exclusive. Marks the end of the timeslot that will be reserved in the listing's availability. Available in the transaction process as `:time/booking-end` timepoint. - `bookingDisplayStart`: timestamp, optional, must be given if `bookingDisplayEnd` is given Moment of time that is displayed to a user as a booking start time. Does not affect availability of the listing. If not given, defaults to the value of `bookingStart` (normalized to UTC midnight, when `type` is `:day`). Available in the transaction process as `:time/booking-display-start` timepoint. - `bookingDisplayEnd`: timestamp, optional, must be given if `bookingDisplayStart` is given Moment of time that is displayed to a user as a booking end time. Does not affect availability of the listing. If not given, defaults to the value of `bookingEnd` (normalized to UTC midnight, when `type` is `:day`). Available in the transaction process as `:time/booking-display-end` timepoint. - `seats`, integer, optional The number of seats that the booking reserves in the listing's availability. **Configuration options**: - `type`: enum, one of `:day`, `:time`. Defaults to `:day`. If set to `:day` normalizes `bookingStart` and `bookingEnd` values to midnight UTC. The value here should match the booking type set for the action that created the booking earlier in the transaction process. #### `:action/cancel-booking` Cancel an accepted booking. **Preconditions**: - Transaction must have a booking - Booking must be in the accepted state **Parameters**: - **Configuration options**: - #### `:action/decline-booking` Decline a pending or proposed booking. **Preconditions**: - Transaction must have a booking - Booking must be in either `pending` or `proposed` state **Parameters**: - **Configuration options**: - ### Stock reservations #### `:action/create-pending-stock-reservation` Creates a new stock reservation in `pending` state with given stock, as long as the listing's current stock permits the reservation to be created. A stock reservation in `pending` state decreases the listing's stock - a stock adjustment is created having the given `stockReservationQuantity` (with a negative sign). There can be at most one stock reservation per transaction. **Preconditions**: - The transaction must not already have a stock reservation. - The listing must have sufficient stock available. **Parameters**: - `stockReservationQuantity`: positive integer, mandatory. The quantity of stock that is reserved from a listing's stock. **Configuration options**: - #### `:action/create-proposed-stock-reservation` Creates a new stock reservation in `proposed` state with given stock, as long as the listing's stock permits the reservation to be created. A stock reservation in `proposed` state does not affect the listing's available stock, i.e. the listing's stock is not decreased by the proposed reservation quantity until the reservation is accepted using the `:action/accept-stock-reservation` action. There can be at most one stock reservation per transaction. **Preconditions**: - The transaction must not already have a stock reservation. - The listing must have sufficient stock available. **Parameters**: - `stockReservationQuantity`: positive integer, mandatory. The quantity of stock for the reservation. **Configuration options**: - #### `:action/accept-stock-reservation` Mark stock reservation as accepted. A stock reservation in `accepted` state decreases the listing's stock. That means that if the stock reservation was previously in a `proposed` state, a new stock adjustment is created with the quantity of the stock reservation (with a negative sign). **Preconditions**: - The transaction must have a stock reservation - The stock reservation must be in either `proposed` or `pending` state - If the stock reservation is in `proposed` state, the listing must have sufficient stock so that the stock reservation can be accepted. **Parameters**: **Configuration options**: - #### `:action/decline-stock-reservation` Decline a pending or proposed stock reservation. If the stock reservation was previously in a `pending` state, a new stock adjustment is created for the listing, reversing the adjustment that resulted from the stock reservation being created. As a result, the stock reservation will have no net impact on the listing's available stock. Note: It is recommended to place this action last in the transition's action list whenever possible. **Preconditions**: - The transaction must have a stock reservation - The stock reservation must be in either `proposed` or `pending` state **Parameters**: **Configuration options**: - #### `:action/cancel-stock-reservation` Cancel an accepted stock reservation. A new stock adjustment is created for the listing, reversing the adjustment that resulted from the stock reservation being created or accepted. As a result, the stock reservation will have no net impact on the listing's available stock. Note: It is recommended to place this action last in the transition's action list whenever possible. **Preconditions**: - The transaction must have a stock reservation - The stock reservation must be in `accepted` state **Parameters**: **Configuration options**: - ### Reviews #### `:action/post-review-by-customer` Action for customer to post a review of provider. The review is left in pending state. Executing `:action/publish-reviews` will make them publicly available. **Preconditions**: - Transaction must not be reviewed by customer already **Parameters**: - `reviewRating`, integer from 1 to 5, mandatory - `reviewContent`, written review, string, mandatory **Configuration options**: - #### `:action/post-review-by-provider` Action for provider to post a review of customer. The review is left in pending state. Executing `:action/publish-reviews` will make them publicly available. **Preconditions**: - Transaction must not be reviewed by provider already **Parameters**: - `reviewRating`, integer from 1 to 5, mandatory - `reviewContent`, written review, string, mandatory **Configuration options**: - #### `:action/publish-reviews` Action to publish any reviews in the transaction. **Preconditions**: - **Parameters**: - **Configuration options**: - ### Extended data #### `:action/reveal-customer-protected-data` Merge customer protected data into transaction protected data. **Preconditions**: - **Parameters**: - **Configuration options**: - `key-mapping`: map (keyword -> keyword), optional Defines which keys are revealed to the transaction. Also can be used to rename keys so that the transaction's protected data will use different keys than the user's protected data does. For example,the following config will reveal customer's phoneNumber attribute to the transaction and rename it to customerPhoneNumber: ``` {:key-mapping {:phoneNumber :customerPhoneNumber}} ``` More than one key can be revealed at the same time: ``` {:key-mapping {:phoneNumber :customerPhoneNumber :address :customerAddress}} ``` Renaming is optional. If you wish to keep the same key, you can repeat it as the value: ``` {:key-mapping {:address :address}} ``` #### `:action/reveal-provider-protected-data` Merge provider protected data into transaction protected data. **Preconditions**: - **Parameters**: - **Configuration options**: - `key-mapping`: map (keyword -> keyword), optional Defines which keys are revealed to the transaction. Also can be used to rename keys so that the transaction's protected data will use different keys than the user's protected data does. For example,the following config will reveal provider's phoneNumber attribute to the transaction and rename it to providerPhoneNumber: ``` {:key-mapping {:phoneNumber :providerPhoneNumber}} ``` More than one key can be revealed at the same time: ``` {:key-mapping {:phoneNumber :providerPhoneNumber :address :providerAddress}} ``` Renaming is optional. If you wish to keep the same key, you can repeat it as the value: ``` {:key-mapping {:address :address}} ``` #### `:action/update-protected-data` Merge given data to the protected data of the transaction. **Preconditions**: - **Parameters**: - `protectedData`: JSON object, max 50KB, optional **Configuration options**: - #### `:action/privileged-update-metadata` Merge given data into the metadata of the transaction. This action requires that the transition is made from a trusted context. **Preconditions**: - **Parameters**: - `metadata`: JSON object, max 50KB, optional **Configuration options**: - ### Stripe integration #### `:action/stripe-create-payment-intent` Action for creating a Stripe PaymentIntent for the transaction. This action supports card payments or equivalent (such as using wallets like Google Pay and Apple Pay). Payment Intents are the main supported way to collect payments. Transaction processes need to use them especially if they want to be SCA-compatible. Only one PaymentIntent per transaction is supported. After the PaymentIntent is created, it's ID and client secret are temporarily accessible in the Sharetribe transaction protected data under the `stripePaymentIntents` key in the following form: ```json { "default": { "stripePaymentIntentId": "pi_1EXSEzLSea1GQQ9x5PnNTeuS", "stripePaymentIntentClientSecret": "pi_1EXSEzLSea1GQQ9x5PnNTeuS_secret_xxxxxxxxxxxxxxxxxxxxxxxxx" } } ``` Client applications can use the PaymentIntent client secret in order to handle payment and confirm the PaymentIntent client-side. The `stripePaymentIntents` key is removed from the protected data once the `:action/stripe-confirm-payment-intent` is used. **Preconditions**: - The transaction must already have pricing information (i.e. payin and payout totals) calculated. - The calculated payin must be greater than zero - The calculated payin must be greater than or equal to the payout - If `:use-customer-default-payment-method?` is set `true`, customer must have Stripe Customer and default payment method set. **Parameters:** - `paymentMethod`, string, optional. Stripe PaymentMethod ID of payment method to be used in the payment. If not given, client is responsible for attaching a PaymentMethod to the PaymentIntent via e.g. Stripe.js SDK. - `setupPaymentMethodForSaving`, boolean, optional. If set to true, the PaymentIntent is created in such a way so that the PaymentMethod used in the payment can be later attached to a Stripe Customer. Otherwise, the PaymentMethod (if new) can not be used in any other way again after the payment. After payment is done via the transaction process, the client can use Marketplace API operations `/stripe_customer/add_payment_method` or `/stripe_customer/create` to attach the payment method to a Customer or create a new Customer if one didn't exist. **Configuration options:** - `:use-customer-default-payment-method?`, boolean, optional. If set to `true`, the payment intent is created using the Customer's default payment method and it is created as an off-session payment in Stripe (i.e. customer not present). Intended to be used in transitions where customer is not present (i.e. delayed transitions, or ones triggered by operator or provider). This tells Stripe to attempt to exempt the payment from SCA. However, the bank in question may still require authentication. If the bank requires authentication from the customer, or declines the charge for any reason, the Stripe API call will fail and the transition using this action will fail. The action also automatically confirms the payment intent, i.e. `:action/stripe-confirm-payment-intent` must not be included in this or subsequent transitions. #### `:action/stripe-create-payment-intent-push` Action for creating a Stripe Payment Intent for use with synchronous push payment methods. These are some of the payment methods that are supported: - Alipay - Affirm - Bancontact - EPS - giropay - iDEAL - Klarna - PayPal - Przelewy24 Note that this is not an exhaustive list of payment methods supported by Stripe. Sharetribe supports payment methods that have immediate payment confirmation and that are supported by [Stripe's PaymentIntents API](https://stripe.com/en-fi/guides/payment-methods-guide#2-choosing-the-right-payment-methods-for-your-business). These tend to fall into the following categories: cards, bank transfers, digital wallets, and BNPL (buy now, pay later). See [Stripe's payment method support documentation](https://docs.stripe.com/payments/payment-methods/payment-method-support) for more extensive list of payment methods, keeping the above criteria in mind. Only one PaymentIntent per transaction is supported. After the PaymentIntent is created, it's ID and client secret are temporarily accessible in the transaction protected data under the `stripePaymentIntents` key in the following form: ```json { "default": { "stripePaymentIntentId": "pi_1EXSEzLSea1GQQ9x5PnNTeuS", "stripePaymentIntentClientSecret": "pi_1EXSEzLSea1GQQ9x5PnNTeuS_secret_xxxxxxxxxxxxxxxxxxxxxxxxx" } } ``` Client applications can use the PaymentIntent client secret in order to handle payment client-side. Typically that involves using Stripe.js SDK to attach a payment method and handle the bank redirect where the customer confirms the payment. The `stripePaymentIntents` is removed from the protected data once the `:action/stripe-confirm-payment-intent` is used. Payments with synchronous push payment methods are captured and made in full immediately when confirmed by the customer via their bank or app. Unlike with card payments, there is no preauthorization stage. This means that when using a push payment intent, `:action/stripe-confirm-payment-intent` is required but `:action/stripe-capture-payment-intent` is not. After the payment is made, it can only be reversed with a full refund using the `:action/stripe-refund-payment`. Unlike cancelling a preauthorization, **creating a full refund does not refund Stripe's own payment processing fees to the Stripe platform account**. Therefore, it is recommended that the transaction process takes that into account. Typically, this means that the transaction process is some form of "instant booking" where provider does not need to accept the booking at all, or alternatively a process where the provider accepts the booking before the payment is made. **Preconditions**: - The transaction must already have pricing information (i.e. payin and payout totals) calculated. - The calculated payin must be greater than zero - The calculated payin must be greater than or equal to the payout **Parameters:** - `paymentMethodTypes`, array of strings, mandatory. List of payment method types that are allowed to be used to complete this payment intent. Must be one or more of the following: `alipay`, `bancontact`, `eps`, `giropay`, `ideal`, `p24`. - `paymentMethod`, string, optional. Stripe PaymentMethod ID of payment method to be used in the payment. If not given, client is responsible for attaching a PaymentMethod to the PaymentIntent via e.g. Stripe.js SDK. **Note** that the allowed payment method types are passed as transition parameters. If implementations wish to strictly validate which payment methods are allowed to fulfill a payment, use a privileged transition and validate the set of allowed payment methods in your server. **Configuration options:** - #### `:action/stripe-confirm-payment-intent` Action for confirming payment intent that is in pending state. If the payment intent was created with `:action/stripe-create-payment-intent` (a card payment), a preauthorization is placed on the card. The payment then can be captured in full by using `:action/stripe-capture-payment-intent` within 7 days of creating the payment intent, or the preauthorization can be released by using `:action/stripe-refund-payment`. Payments with synchronous push payment methods (created with `:action/stripe-create-payment-intent-push`) are captured and made in full immediately when confirmed by the customer via their bank or app and unlike card payments there is no preauthorization stage. After the payment is made, it can only be reversed with a full refund using `:action/stripe-refund-payment`. Unlike cancelling a preauthorization, creating a full refund does not refund Stripe's own payment processing fees to the Stripe platform account. Therefore, it is recommended that the transaction process takes that into account. Typically, this means that the transaction process is some form of "instant booking" where provider does not need to accept the booking at all, or alternatively a process where the provider accepts the booking before the payment is made. **Preconditions**: - Transaction must have a payment created with `:action/stripe-create-payment-intent` or `:action/stripe-create-payment-intent-push`. **Parameters:** - **Configuration options:** - #### `:action/stripe-capture-payment-intent` Action for capturing a confirmed Stripe PaymentIntent. If the PaymentIntent was created with `:action/stripe-create-payment-intent-push`, the PaymentIntent is captured automatically already when the payment is confirmed and this action will have no effect. Uncaptured payment intents are valid for seven days, after which they are automatically canceled by Stripe. **Preconditions:** - Transaction must have a payment that has been confirmed with `:action/stripe-confirm-payment-intent` - Provider must have connected Stripe account **Parameters:** - **Configuration options:** - #### `:action/stripe-create-payout` Create a pay out to external bank account. The managed account must have sufficient available balance. The timing of the payout depends on when the money is available in Stripe. If the money is already available when this action is triggered, the payout happens immediately. If the money is not yet available, the payout is scheduled based on the time when Stripe indicates that the balance will be available. **Preconditions**: - Transaction must have pay-out value set - Transaction must have a Stripe transfer **Parameters**: - **Configuration options**: - #### `:action/stripe-refund-charge` **DEPRECATED**: same as `:action/stripe-refund-payment`, use that instead #### `:action/stripe-refund-payment` Refund (in full) a Stripe payment. Supports both cancelling a PaymentIntent that has not yet been captured, as well as issuing a Stripe refund for the charge if the PaymentIntent was captured. **Preconditions**: - The transaction must have a payment created with either `:action/stripe-create-payment-intent` or `:action/stripe-create-payment-intent-push` - The transaction must not yet have passed through a transition using `:action/stripe-create-payout` **Parameters**: - **Configuration options**: - ### Testing #### `:action/fail` Action that always fails. Useful for testing. **Preconditions**: - **Parameters**: - **Configuration options**: - --- ## Transaction process format Path: references/transaction-process-format/index.mdx # Transaction process format This reference guide assumes you know the basic idea and concepts, such as states, transitions and actions, of the Sharetribe transaction process. If you don't, please check this [background article about Transaction process](/concepts/transactions/transaction-process/) first. ## Example process Let's start with an example process that looks like this: ![Example process](./example-process.png) The annotated process description for this process is as follows: ```clojure { ;; Tag to set the process description format. ;; It's always :v3. Earlier versions are deprecated. :format :v3 ;; The process graph defined as transitions between states. The states are implicitly defined by transitions. ;; Note that the graph has to be connected, i.e. it's a single flow with branches, not multiple graphs. :transitions [{ ;; Transition name, has to be unique. Used in API calls to create and transition transactions. :name :transition/request-payment ;; Who has the permission to execute the transition. One of: :actor.role/customer, :actor.role/provider, :actor.role/operator. Operator role means that the transition is executed by a marketplace operator on Console UI. :actor :actor.role/customer ;; Privileged transitions require that they are done in a trusted context, which ;; typically means using special access token when calling the API. :privileged? true ;; The actions that the transaction engine executes when the transition is taken. :actions [{:name :action/create-pending-booking :config {:type :time}} {:name :privileged-set-line-items} {:name :action/stripe-create-payment-intent}] ;; The state to which the process transitions to when the transition is completed. :to :state/pending-payment ;; This transition doesn't have a :from state because this is an "initial transition", ;; i.e. a transition that is used when a new transaction is created. } {:name :transition/expire-payment ;; Timing for the transition. This is a delayed transition that the system automatically executes at the defined time. ;; For delayed transitions we don't define an :actor. :at {:fn/plus [{:fn/timepoint [:time/first-entered-state :state/pending-payment]} {:fn/period ["PT15M"]}]} :actions [{:name :action/decline-booking} {:name :action/calculate-full-refund} {:name :action/stripe-refund-payment}] ;; The state from which this transition can be executed. :from :state/pending-payment :to :state/payment-expired} {:name :transition/confirm-payment, :actor :actor.role/customer :actions [{:name :action/stripe-confirm-payment-intent}], :from :state/pending-payment :to :state/preauthorized} {:name :transition/accept :actor :actor.role/provider :actions [{:name :action/accept-booking} {:name :action/stripe-capture-payment-intent}] :from :state/preauthorized :to :state/accepted} {:name :transition/decline :actor :actor.role/provider :actions [{:name :action/decline-booking} {:name :action/calculate-full-refund} {:name :action/stripe-refund-payment}] :from :state/preauthorized :to :state/declined} {:name :transition/expire :at {:fn/min [{:fn/plus [{:fn/timepoint [:time/first-entered-state :state/preauthorized]} {:fn/period ["P6D"]}]} {:fn/plus [{:fn/timepoint [:time/booking-end]} {:fn/period ["P1D"]}]}]} :actions [{:name :action/decline-booking} {:name :action/calculate-full-refund} {:name :action/stripe-refund-payment}] :from :state/preauthorized :to :state/declined} {:name :transition/complete :at {:fn/timepoint [:time/booking-end]} :actions [{:name :action/stripe-create-payout}] :from :state/accepted :to :state/delivered} {:name :transition/cancel :actor :actor.role/operator :actions [{:name :action/cancel-booking} {:name :action/calculate-full-refund} {:name :action/stripe-refund-payment}] :from :state/accepted :to :state/cancelled}] ;; Notifications (emails) that are sent or scheduled when a transition is completed. :notifications [{ ;; Unique name of the notification. :name :notification/new-booking-request ;; The transition that when completed triggers this notification. :on :transition/confirm-payment ;; The transaction party this notification is sent to. Options are :actor.role/provider and :actor.role/customer. :to :actor.role/provider ;; Name of the email template for creating the email content. :template :new-booking-request} {:name :notification/new-booking-request-reminder :on :transition/confirm-payment :to :actor.role/provider ;; Timing of the notification, meaning this notification is delayed. ;; If the process transitions before the timing, the notification is not sent. ;; Useful e.g. for reminders before the time window to react to the transaction closes. :at {:fn/min [{:fn/plus [{:fn/timepoint [:time/first-entered-state :state/preauthorized]} {:fn/period ["P5D"]}]} {:fn/timepoint [:time/booking-end]}]} :template :new-booking-request-reminder} {:name :notification/booking-request-accepted :on :transition/accept :to :actor.role/customer :template :booking-request-accepted} {:name :notification/booking-request-declined :on :transition/decline :to :actor.role/customer :template :booking-request-declined}]} ``` ## The edn format The process description in Sharetribe Sharetribe uses a format called [edn](https://github.com/edn-format/edn). It's quite similar to JSON but it supports a few more primitive types, such as datetime values and keywords, and has some extra features. The syntax is also slightly different from JSON so it might take a bit of time to get used to. Keywords are used heavily in the process description syntax as keys in maps as well as enum values. Keywords start with a `:` but are otherwise similar to strings. Keywords can have a namespace, in which case they are called qualified keywords, or be plain (unqualified). The part before `/` is the namespace. So for example, `:actor.role/customer` is a keyword in the namespace `actor.role`. ## Transitions in the Marketplace API Processes have two different types of transitions. Initial transitions are used for creating new transactions whereas subsequent transitions move existing transactions forward in the process. An initial transition in the process definition has no `:from` state defined. When we render the process graph on [Console](https://console.sharetribe.com) we show a synthetic state `state/initial` but this is not a state that is or should be defined in the process description. In Marketplace API initial transitions are invoked via the [transactions/initiate](https://www.sharetribe.com/api-reference/marketplace.html#initiate-transaction) endpoint and subsequent transitions via the [transactions/transition](https://www.sharetribe.com/api-reference/marketplace.html#transition-transaction) endpoint. The API also provides endpoints for invoking transitions speculatively: [transitions/initiate_speculative](https://www.sharetribe.com/api-reference/marketplace.html#speculatively-initiate-transaction) and [transactions/transition_speculative](https://www.sharetribe.com/api-reference/marketplace.html#speculatively-transition-transaction). Speculative operations take the same parameters that the real initiate and transition endpoints take but only simulate the effects. In other words, no state is changed, Stripe is not really called, etc.. However, they do run the same full validations on parameters as well as execute the [action preconditions checks](#preconditions) and return errors in case of failures. When the transition completes successfully, the speculation operations also return simulated results that show how the transaction object will look like after a real initiate or transition operation. ## Transitions in the Integration API It is possible to use the [Integration API](/concepts/api-sdk/marketplace-api-integration-api/) to invoke transitions, which are defined as having `:actor.role/operator` as the `:actor`. This is done via the [transactions/transition](https://www.sharetribe.com/api-reference/integration.html#transition-transaction) endpoint. Unlike the Marketplace API, the Integration API currently does not provide an endpoint for initiating transactions. Similarly to the Marketplace API, the Integration API also provides an endpoint to [invoke transitions speculatively](https://www.sharetribe.com/api-reference/integration.html#speculatively-transition-transaction). The Integration API provides a trusted context for invoking transitions. This means that these transitions are considered [privileged](/concepts/transactions/privileged-transitions/) and can utilize any of the actions that require a trusted context. ## Action composition Each transition defines an ordered list of 0 or more actions. Actions are instructions for the transaction engine and define what happens when a transition is executed. The ordering of the actions matters because they are executed in the given order. In the above example process we define a transition from `:state/accepted` to `:state/cancelled` like this: ```clojure {:name :transition/cancel :actor :actor.role/operator :actions [{:name :action/cancel-booking} {:name :action/calculate-full-refund} {:name :action/stripe-refund-payment}] :from :state/accepted :to :state/cancelled} ``` This means that the first action to execute is `:action/cancel-booking` which, like you might have guessed, marks the booking associated with the transaction as cancelled. Next we calculate a full refund and add the information to the transaction as line items. Finally, we invoke a payment refund via Stripe. When all of the above mentioned steps are taken and complete successfully, the transition is completed and the process moves to state `:state/cancelled`. ### Preconditions Actions cannot be composed arbitrarily. Each action defines zero or more preconditions that must be met for the action to run successfully. If any of these are not met at the time the action is invoked, the action will fail which in turn fails the transition. In our example above, the `:action/cancel-booking` has a precondition that the process must contain a booking and that booking must be in state accepted. This means that at some earlier point in the process we must have invoked the actions `:action/create-pending-booking` followed by `:action/accept-booking`. However, these could all happen during a single transition. That's a bit contrived example but should help to understand the limits and opportunities with composing actions. ### Parameters Every action can define zero or more parameters. The action parameters are passed via [the Marketplace API](https://www.sharetribe.com/api-reference/marketplace.html#transactions) when a transition is invoked. Some of the action parameters are mandatory and some are optional. All the mandatory and optional parameters of the actions together define the parameters of the transition. In our example process, the `:transition/request-payment` defines the actions `action/create-pending-booking`, `action/privileged-set-line-items` and `action/stripe-create-payment-intent`. This means the transition requires and accepts the following parameters defined by the `action/create-pending-booking`: - `bookingStart`, `bookingEnd`: timestamp, mandatory - `bookingDisplayStart`, `bookingDisplayEnd`: timestamp, optional as well as the following parameters defined by the `action/privileged-set-line-items`: - `lineItems`, [line item array](/references/transaction-process-actions/#pricing), mandatory plus the following parameters defined by the `action/stripe-create-payment-intent`: - `paymentMethod`: string, mandatory - `setupPaymentMethodForSaving`: boolean, optional, defaults to `false` ### Configuration options Some actions support configuration options that alter their behaviour. For example, the `action/create-pending-booking` takes configuration parameter for the type of the booking being created. Other examples are the commission calculation actions that take the commission percentage as a configuration option. The configuration options for an action are set in the process description via the `:config` key in the action definition: ```clojure { ;; Name of the action. :name :action/create-pending-booking ;; The configuration options map. ;; Can be omitted if no options need to be passed. :config {:type :time}} ``` You can see all the preconditions, action parameters and configuration options for each action in the [Transaction process actions](/references/transaction-process-actions/) reference article. ## Time expressions, delayed transitions and delayed notifications Time expressions can be used both with transitions and notifications to delay the execution. The Sharetribe transaction engine exposes a set of timepoints that you can tie delays to as well as a small set of functions to further control the exact timing. The basic structure of a time expression is a map from function name to a list (vector) of function parameters: `{:fn/function-name [function-param1 function-param2]}`. For example: `{fn/timepoint [:time/booking-end]`. When a delayed transition or notification is scheduled, it will execute at the resulting time. However, if the transition moves forward before the scheduled moment, the operation is automatically cancelled. This way you can send a reminder notification or schedule an automatic cancellation after a certain time period that will be executed only in the case that nobody takes action before that. Also, if the scheduled time is in the past the operation will execute immediately. By wrapping the time expression with `:fn/ignore-if-past` you can instead ignore operations when the scheduled time is in the past. You can see a full list of timepoints and timepoint expression functions in the [Transaction process time expressions](/references/transaction-process-time-expressions/) reference article. Note that your transaction process can have several automatic transitions _scheduled_ for a state, but only one automatic transition _executed_ for a state. You may have e.g. one automatic transition scheduled to execute 1 day after first entering the state, and another scheduled to execute 7 days before a booking starts. The transition that gets executed is the one whose time point is matched first. However, if the first automatic transition fails for some reason, no further automatic transitions get executed from the state. ## Notifications Notifications are emails that are sent as part of the transaction process when certain transitions occur. They optionally support delays via the `:at` key (see also the [Time expressions, delayed transitions and delayed notifications](#time-expressions-delayed-transitions-and-delayed-notifications) section). Every notification needs to have a unique (in the scope of the process) name and can be tied only to a single transition. The email content of a notification is rendered using a template. These templates can be reused between notifications. If there's two different transitions where you want to send the same email, you can just refer to same template in both. Notifications can be sent to the customer or to the provider. ## Validating and inspecting a process The Sharetribe CLI supports validating a local process description as well as showing basic information about the process and its transitions. To validate a process and print overall process description (when it's valid) or validation errors: ```bash flex-cli process --path my-process-dir ``` To print more details about a specific transition: ```bash flex-cli process --path my-process-dir --transition transition/my-transition ``` Assuming we have stored the example process from this guide under (./processes/guide/example/process.edn) we can inspect the request-payment transition by running: ```ansi $ flex-cli process --path processes/guide/example --transition transition/request-payment Name transition/request-payment From state/initial To state/pending-payment Actor Customer At - Actions Name Config :action.initializer/init-listing-tx :action/create-pending-booking {:type :time} :action/privileged-set-line-items :action/stripe-create-payment-intent Notifications - ``` Once you've pushed a new process version to your marketplace you can use the [Console process viewer](https://console.sharetribe.com/advanced/transaction-processes) to see the process graph and inspect transitions. This is currently the only place where you can see the parameters that a given transition requires and accepts. ## Process reference ### Process | Key | Type | Description | Example | | ---------------- | ----------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------- | | `:format` | Keyword | Process format, always `:v3` | `:v3` | | `:transitions` | Vector (order doesn't matter) | A list of transitions that the process consists of. Implicitly define states. | `[{:name :transition/request-payment ...} ...]` | | `:notifications` | Vector (order doesn't matter) | A list of notifications for the process. | `[{:name :notification/new-booking-request ...} ...]` | ### Transition | Key | Type | Description | Example | | -------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | | `:name` | Keyword | Unique name for the transition. Used when invoking the transition via Marketplace API. | `:transition/request-payment` | | `:actor` | Keyword | Defines who has the permission to invoke the transition. Must be one of: `:actor.role/customer`, `:actor.role/provider`, `:actor.role/operator` | `:actor.role/customer` | | `:actions` | Vector | An ordered list of actions to take when the transition is executed. | `[{:name :action/create-pending-booking ...} ...]` | | `:from` | Keyword | Name of the state from which the transition can be taken from. Left out for initial transitions. | `:state/pending-payment` | | `:to` | Keyword | Name of the state to which this transition leads. | `:state/pending-payment` | | `:at` | Time expression | Optional time expression that when given, turns the transition to a delayed transition. When using `:at` do not specify `:actor` | `{:fn/timepoint [:time/booking-end]}` | | `:privileged?` | Boolean | Optionally mark the transition as [privileged](/concepts/transactions/privileged-transitions/). Privileged transitions can only be invoked from a trusted context and are useful when you need to ensure the transition parameters are correct or have specific values. | `true` | **Example**: ```clojure {:name :transition/transition-name :actor :actor.role/customer ;; actor.role/provider or :actor.role/operator :actions [] :from :state/from-state :to :state/to-state} ``` ### Action | Key | Type | Description | Example | | --------- | ------- | -------------------------------------------------------- | -------------------------------- | | `:name` | Keyword | Reference to an action to use. | `:action/create-pending-booking` | | `:config` | Map | A map from action configuration options to their values. | `{:type :time}` | **Example**: ```clojure {:name :action/create-pending-booking :config {:type :time}} ``` ### Notification | Key | Type | Description | Example | | ----------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `:name` | Keyword | Unique name for the notification. | `:notification/new-booking-request` | | `:on` | Keyword | Reference to a transition name that when completed triggers this notification. | `:transition/confirm-payment` | | `:to` | Keyword | Recipient of the notification email. One of: `:actor.role/customer`, `:actor.role/provider` | `:actor.role/provider` | | `:template` | Keyword | Reference to an email template to render the email body for this notification. | `:new-booking-request` | | `:at` | Time expression | Optional time expression that when given turns the notification to a delayed notification. Any scheduled delayed notification is cancelled if the transaction transitions to another state before the notification is sent. | `{:fn/plus [{:fn/timepoint [:time/first-entered-state :state/preauthorized]} {:fn/period ["P5D"]}]}` | **Example**: ```clojure {:name :notification/notification-name :on :transition/transition-name :to :actor.role/customer ;; or :actor.role/provider :template :email-template-name} ``` --- ## Transaction process time expressions Path: references/transaction-process-time-expressions/index.mdx # Transaction process time expressions Time expressions can be used both with transitions and notifications to delay the execution. The Sharetribe transaction engine exposes a set of timepoints that you can tie delays to as well as a small set of functions to further control the exact timing. The basic structure of a time expression is a map from function name to a list (vector) of function parameters: `{:fn/function-name [function-param1 function-param2]}`. For example: `{fn/timepoint [:time/booking-end]`. When a delayed transition or notification is scheduled, it will execute at the resulting time. However, if the transition moves forward before the scheduled moment, the operation is automatically cancelled. This way you can send a reminder notification or schedule an automatic cancellation after a certain time period that will be executed only in the case that nobody takes action before that. Also, if the scheduled time is in the past the operation will execute immediately. By wrapping the time expression with `:fn/ignore-if-past` you can instead ignore operations when the scheduled time is in the past. Note that your transaction process can have several automatic transitions _scheduled_ for a state, but only one automatic transition _executed_ for a state. You may have e.g. one automatic transition scheduled to execute 1 day after first entering the state, and another scheduled to execute 7 days before a booking starts. The transition that gets executed is the one whose time point is matched first. However, if the first automatic transition fails for some reason, no further automatic transitions get executed from the state. To learn more how to use time expressions in the transaction process, see the [Transaction process format](/references/transaction-process-format/) reference article. ## Time functions | Fn | Arguments | Description | Example | | -------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `:fn/timepoint` | timepoint name + timepoint args (if any) | Returns the timestamp of a timepoint in the process. | `{:fn/timepoint [:time/first-entered-state :state/state-name]}` | | `:fn/period` | Period expression, string | A timespan that can be added or subtracted. | `{:fn/period ["PT15M"]}` | | `:fn/min` | Two or more timestamps. | Returns the smallest (earliest) of all given timestamps. | `{:fn/min [{:fn/timepoint [:time/booking-start]} {:fn/timepoint [:time/booking-display-start]}]}` | | `:fn/plus` | A timestamp + 1 or more timespans | Returns the timestamp with all the timespans added to it. | `{:fn/plus [{:fn/timepoint [:time/booking-start]} {:fn/period "P1D"}]}` | | `:fn/minus` | A timestamp + 1 or more timespans | Returns the timestamp with all the timespans subtracted from it. | `{fn/minus [{:fn/timepoint [:time/booking-end]} {:fn/period "P2D"}]`:} | | `:fn/ignore-if-past` | A timestamp | Returns the given timestamp as is unless it's in the past, in which cases returns nothing. Returning nothing from a time expression cancels scheduling the operation. | `{:fn/ignore-if-past [{:fn/timepoint [:time/booking-start]}]}` | The time functions can be nested freely. ## Period expressions The function `:fn/period` accepts an ISO 8601 duration as string. For more about the ISO 8601 duration format see: https://en.wikipedia.org/wiki/ISO_8601#Durations ## Timepoints | Timepoint | Arguments | Description | Example | | ----------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | | `:time/tx-initiated` | - | The timestamp for when the transaction was initiated. | `{:fn/timepoint [:time/tx-initiated]}` | | `:time/first-entered-state` | State name as keyword. | The timestamp when the process entered the given state for the first time. Processes may contain loops so a state can be entered multiple times. | `{:fn/timepoint [:time/first-entered-state :state/state-name]}` | | `:time/last-entered-state` | State name as keyword. | The timestamp when the process entered the given state for the most recent time. Processes may contain loops so a state can be entered multiple times. | `{:fn/timepoint [:time/last-entered-state :state/state-name]}` | | `:time/first-transitioned` | Transition name as keyword. | The timestamp when the process executed the given transition successfully for the first time. | `{:fn/timepoint [:time/first-transitioned :transition/transition-name]}` | | `:time/booking-start` | - | The booking start timestamp. | `{:fn/timepoint [:time/booking-start]}` | | `:time/booking-end` | - | The booking end timestamp. | `{:fn/timepoint [:time/booking-end]}` | | `:time/booking-display-start` | - | The booking display start timestamp. | `{:fn/timepoint [:time/booking-start]}` | | `:time/booking-display-end` | - | The booking display end timestamp. | `{:fn/timepoint [:time/booking-end]}` | A booking always has start and end times. You may optionally specify display start and display end times via transition parameters. Start and end times are used by all actions that automatically calculate prices based on booking length. The automatic availability management also relies on start and end times. The display versions of the start and end times are just for showing to users in the UI but you can additionally use them in time expressions and transaction email templates. Finally, if display times are not specified via transition params they default to booking start and end times. --- ## Enable analytics Path: template/analytics/how-to-set-up-analytics-for-template/index.mdx # Enable analytics The Sharetribe Web Template comes with built-in support for Google Analytics and supports tracking page views with a customizable analytics handler. The template also supports using Plausible analytics for tracking page views. This article explains how to enable Plausible or Google Analytics and use and create custom analytics handlers. ## Configure Google Analytics The template has built-in support for Google Analytics. All you need to do is assign your Google Analytics Tracking ID to the environment variable `REACT_APP_GOOGLE_ANALYTICS_ID`. Google Analytics doesn't work in a hot-loading environment! The analytics script is added server-side. You can test it in your local environment by using the command `yarn run dev-server`. ### Google Analytics 4 Google recently released their new analytics service Google Analytics 4. Support for Google Universal Analytics will end on October 1, 2023. New versions of the template provide out-of-the-box support for Google Analytics 4. If you prefer to use Universal Analytics, you should look into how Analytics was implemented in [this pull request](https://github.com/sharetribe/ftw-daily/pull/1508). The template will require a Tracking ID compatible with Google Analytics 4. The ID needs to begin with the "G-" prefix. #### Enhanced measurements It is not recommended to use the Enhanced Measurements feature introduced in Google Analytics 4, which is enabled by default. The Enhanced Measurements feature injects code into link tags which can break in-app navigation in the template. Therefore, we strongly recommend disabling the Enhanced Measurements feature when using Google Analytics 4 with the template. If that's not an option, you can continue to use Enhanced Measurements if you disable the _Outbound clicks_ and _page changes based on browser history events_ features. ![Disable Outbound clicks](./disable.png) ### Built-in handlers The Sharetribe Web Template includes an [event handler](https://github.com/sharetribe/web-template/blob/main/src/analytics/handlers.js#L9) that sends `page_view` events to Google Analytics. These events need to registered manually because the template is a single-page application, meaning that in-app navigation does not render a page load. The Google Analytics script registers a `page_view` event automatically on every page load. The [`trackPageView`](https://github.com/sharetribe/web-template/blob/main/src/analytics/handlers.js#L9) function takes this into account and only sends a `page_view` event to Google if a page is accessed through in-app navigation. If you'd like to track something other than page views, you can implement your custom handler using the `trackPageView` function as an example. ## Configure Plausible Analytics The template also includes built-in support for [Plausible Analytics](https://plausible.io). Plausible is a GDPR compliant open source web analytics service. You must assign the data-domain value from the Plausible Analytics tracking script to the environment variable `REACT_APP_PLAUSIBLE_DOMAINS`. If this would be your Plausible analytics script: `` You would assign example.com to the environment variable: `REACT_APP_PLAUSIBLE_DOMAINS=example.com` You can also assign multiple domains separated by a comma: `REACT_APP_PLAUSIBLE_DOMAINS=example1.com,example2.com` Without customising how Plausible is [loaded into the template](https://github.com/sharetribe/web-template/blob/fd9596462c5979ca9e421b1ab69df92a7dd2056c/src/util/includeScripts.js), you can only track page views using it. ```js filename="includeScripts.js" if (plausibleDomains) { // If plausibleDomains is not an empty string, include their script too. analyticsLibraries.push( ); } ``` ## Custom analytics libraries If you choose to add another analytics provider (e.g. Facebook Pixel), you can follow these steps to import the third-party script and create a custom handler. In some cases, it might also be worth looking into npm packages instead of manually appending a third-party script. ### Add the analytics library script If the analytics library has an external script, you can add the library script tag to the [src/util/includeScripts.js](https://github.com/sharetribe/web-template/blob/main/src/util/includeScripts.js) file. You will also need to whitelist the corresponding URLs in the [server/csp.js](https://github.com/sharetribe/web-template/blob/main/server/csp.js) file. #### Example: Add Meta/Facebook Pixel tracking script We suggest including third-party scripts via the [src/util/includeScripts.js](https://github.com/sharetribe/web-template/blob/main/src/util/includeScripts.js) file. You cannot add inline scripts without adding a nonce attribute. See [this pull request](https://github.com/sharetribe/web-template/pull/485) for more information on why nonce needs to be used with inline scripts. Therefore, the recommended approach is to load external scripts dynamically using the script tag. As an example, we will use the Meta Pixel. You will first need to create a script file under `public/static/scripts/facebook/facebook.js` and add the Meta Pixel code. You can find the up to date base code under the [Meta Developer Documentation](https://developers.facebook.com/docs/meta-pixel/get-started/). Refactor the code so that it is structured for use as an external script file: ```js (function () { if (window.fbq) return; (function (f, b, e, v, n, t, s) { if (f.fbq) return; n = f.fbq = function () { n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments); }; if (!f._fbq) f._fbq = n; n.push = n; n.loaded = !0; n.version = '2.0'; n.queue = []; t = b.createElement(e); t.async = !0; t.src = v; s = b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t, s); })( window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js' ); fbq('init', '{your-pixel-id-goes-here}'); fbq('track', 'PageView'); const img = document.createElement('img'); img.height = '1'; img.width = '1'; img.style.display = 'none'; img.src = `https://www.facebook.com/tr?id={your-pixel-id-goes-here}&ev=PageView&noscript=1`; document.body.appendChild(img); })(); ``` Add that code to the facebook.js file. You can then import the file in [src/util/includeScripts.js](https://github.com/sharetribe/web-template/blob/main/src/util/includeScripts.js): ```js analyticsLibraries.push( ); ``` Remember to also update the [csp.js](https://github.com/sharetribe/web-template/blob/main/server/csp.js) file: ```js const { imgSrc = [self] } = defaultDirectives; const imgSrcOverride = imgSrc.concat('www.facebook.com'); const { scriptSrc = [self] } = defaultDirectives; const scriptSrcOverride = scriptSrc.concat( 'connect.facebook.net', 'www.facebook.com' ); const { frameSrc = [self] } = defaultDirectives; const frameSrcOverride = frameSrc.concat( 'connect.facebook.net', 'www.facebook.com' ); const { connectSrc = [self] } = defaultDirectives; const connectSrcOverride = connectSrc.concat('www.facebook.com'); const customDirectives = { imgSrc: imgSrcOverride, scriptSrc: scriptSrcOverride, frameSrc: frameSrcOverride, connectSrc: connectSrcOverride, }; ``` ### Create a handler You can create a custom handler e.g. in [src/analytics/handlers.js](https://github.com/sharetribe/web-template/blob/main/src/analytics/handlers.js). If you want to track page views, you could create a class that implements a `trackPageView(canonicalPath, previousPath)` method. Note that the `canonicalPath` parameter passed to the function might not be the same path as in the URL bar of the browser. It is the canonical form of the URL. **For example**: the listing page URL is constructed dynamically: `l/{listing-slug}/{listing-id}`. The canonical form of that URL would be: `l/{listing-id}`. A "_slug_" is a web development term for a short, user-friendly string. In the example above, the template generates the slug from the listing's title, which is prone to frequent changes. Therefore, a canonical form of that URL is needed to maintain a stable link which doesn't change every time the name of the listing changes. This approach allows unified analytics and correct tracking of pages that can be accessed from multiple URLs. If your analytics library tries to access the page URL directly through the browser, you might need to override that behavior to use the canonical URL that is given to the method. ### Initialise the handler Finally, you only need to initialise the handler in the `setupAnalyticsHandlers()` function in [src/index.js](https://github.com/sharetribe/web-template/blob/main/src/index.js). --- ## Sitemap in Sharetribe Web Template Path: template/analytics/sitemap-in-template/index.mdx # Sitemap in Sharetribe Web Template Your marketplace's sitemap helps search engines to process the correct information on your marketplace. The Sharetribe Web Template has built-in support for creating a sitemap (introduced in [this change set](https://github.com/sharetribe/web-template/pull/243)), and this article gives you some context into how sitemap generating works in the template. ## Template sitemap structure By default, the template generates several files related to the sitemap. When running the template locally with _yarn run dev_, you can find these routes on the dev server port, i.e. localhost:3500. You can read more about the template sitemap and other resources in the [Resources README](https://github.com/sharetribe/web-template/blob/main/server/resources/README.md). ### robots.txt The [your-domain]**/robots.txt** file details the authenticated routes as disallowed for crawlers, and exposes the URL to the main sitemap. If you custom develop routes that require authentication, it is a good idea to specify them here. ### sitemap-index.xml The [your-domain]**/sitemap-index.xml** file further exposes three separate sub-sitemaps. This is useful because it allows you more detailed tracking in analytics tools, and it reduces the size of each individual sitemap. The index and the sub-sitemaps are generated in [server/resources/sitemap.js](https://github.com/sharetribe/web-template/blob/main/server/resources/sitemap.js). ### sitemap-default.xml The [your-domain]**/sitemap-default.xml** file includes your marketplace's public built-in pages. It also includes the landing page, the terms of service page, and the privacy policy page – these pages have specified paths in the default template, even though their content is fetched from assets. If you add public custom paths to your template, remember to [add them in this sitemap](https://github.com/sharetribe/web-template/blob/main/server/resources/sitemap.js#L45) as well. You can also add sitemap entries for some of your key searches, e.g. ```diff const defaultPublicPaths = { landingPage: { url: '/' }, termsOfService: { url: '/terms-of-service' }, privacyPolicy: { url: '/privacy-policy' }, signup: { url: '/signup' }, login: { url: '/login' }, search: { url: '/s' }, + searchMountainBikes: { url: '/s?pub_categoryLevel1=mountainBike' } }; ``` ### sitemap-recent-listings.xml The [your-domain]**/sitemap-recent-listings.xml** file shows the URLs for your marketplace's most recently created listings. The listing data is fetched from the _sdk.sitemapData.queryListings()_ endpoint, and the endpoint returns the 10 000 most recent public listings. The recent listing ids are mapped to the template's default canonical listing URLs. If you make changes to the listing canonical URL structure, be sure to make the corresponding changes to this sitemap as well. ### sitemap-recent-pages.xml The [your-domain]**/sitemap-recent-pages.xml** file shows the URLs for your marketplace's asset-based pages that are shown in _[your-domain]/p/:pathId_. The page information is fetched using the _sdk.sitemapData.queryAssets()_ endpoint, and the endpoint returns the 1 000 most recently created pages. The default logic filters out the pages that have custom paths: landing page, terms of service, and privacy policy. If you add public permanent paths for asset-based pages to your template and your _sitemap-default.xml_ file, be sure to filter them out of the SDK result set in [server/resources/sitemap.js](https://github.com/sharetribe/web-template/blob/main/server/resources/sitemap.js#L284) ## Sitemap caching The template caches sitemap data [for one day by default](https://github.com/sharetribe/web-template/blob/main/server/resources/sitemap.js#L55). In addition, the sitemap API endpoints cache results for one day. This means that when using the template, the combined cache effect can be up to two days. Especially on bigger sites with multiple listings, listing sitemap data can take up to a few seconds to fetch from the SDK. Caching the results improves the sitemap performance. ## Adding your sitemap to Google To add your sitemap on Google, you should add Google site verification through Sharetribe Console. After that, you can add your sitemap index to Google Search Console. That way, your site gets indexed immediately, and you don't need to wait for it to be discovered by bots. You can read more detailed instructions in [Google's own documentation](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap#addsitemap). --- ## Managing listing availability Path: template/availability/availability-management/index.mdx # Managing listing availability This article explains how listing availability is handled in the Sharetribe Web Template, with a focus on technical details and implementation. For a general overview of availability management, see our [Help Center](https://www.sharetribe.com/help/en/articles/8413212-how-availability-management-works). See also the [reference documentation for availability management](/references/availability/). ## Availability plan types and timezone support The Sharetribe APIs support creating availability plans with either type `availabilityPlan/day` with day-level availability or type `availabilityPlan/time` with time range level availability. When a user creates a listing with the Sharetribe Web Template, the listing is always created with type `availabilityPlan/time`, whether the calendar booking type is daily, nightly, hourly or fixed. Using the `availabilityPlan/time` type means that the template can specify a certain time zone for the listing's availability plan, which is asked when creating a bookable listing. The timezone is associated with the availability plan and ensures that all booking times are interpreted according to the local time of the listing, rather than the time zone of the customer making the booking. The `availabilityPlan/day` type always assumes UTC as the timezone. However, there is a trade-off. In order to [query listings](https://www.sharetribe.com/api-reference/marketplace.html#query-listings) that use `availabilityPlan/time`, the `availability` parameter needs to be set to `time-full` or `time-partial`. Queries using this `availability` parameter do not support pagination. By default, the template will display the first 24 results when using the date filter (and as pagination is not supported, the results will be limited to 24). This value can be modified in [SearchPage.duck.js file](https://github.com/sharetribe/web-template/blob/main/src/containers/SearchPage/SearchPage.duck.js#L23), so you can allow displaying up to a 100 results. ![Marketplace payment flow](./dates-query.png) ## Availability management ### Availability plan For daily and nightly listings, the template simplifies availability management by allowing the listing author to select their daily availability. Under the hood, availability is automatically set from `00:00` to `00:00` (i.e., midnight to midnight), enabling bookings to span for consecutive days. For hourly and fixed booking length listings, the template exposes full control over start and end times when adjusting availability entries. These entries can be consecutive or non-consecutive and may have varying seat counts. While it is technically possible to create a continuous availability window from `00:00` to `00:00`, enabling bookings that span entire days or overnight, the template does not have built-in support for creating hourly bookings that span multiple calendar days. ### Availability exceptions Listing authors can set availability exceptions on listings to override the default plan for individual time ranges. Availability exceptions are created based on the unit type: - For daily bookings, exceptions are created per day, and the start and end dates can be the same date - For nightly bookings, exceptions are created per night, and the end date needs to be at least one day later than the start date - For hourly bookings and fixed-length bookings, exceptions are created with a start time and an end time, and the end time needs to be at least an hour later, and on the hour, e.g. 13:00 or 14:00. Regardless of the unit type, all exceptions are made using the same [endpoint](https://www.sharetribe.com/api-reference/marketplace.html#create-availability-exceptions). Listing authors can also block availability by using seats. Setting 0 seats for the duration of the availability exception sets the listing as unavailable for that period. If the period has existing bookings, the availability exception will not affect them, so it only blocks future bookings. The availability plan, availability exceptions, and bookings are only visible to the author of the listing. When other users view the listing’s availability on the listing page, they only see whether the listing is available, but they cannot see whether the availability results from the availability plan, exceptions, or existing bookings. ## Listing search and timezone coverage The operator can allow users to search for listings based on their availability on certain dates. The date search cannot be restricted to specific time zones. This means that to cover listings from all possible time zones, the template prolongs the start and end times before sending them to the Sharetribe API. See the relevant code [in SearchPage.duck.js](https://github.com/sharetribe/web-template/blob/main/src/containers/SearchPage/SearchPage.duck.js#L181-L182). To be exact, the start time is moved 14 hours earlier, and the end time is moved 12 hours later. This means that the availability search results may include listings that are available before or after the time frame that the user enters. ![Time range](./time-range.png) ## Filtering timeslot with intervals When querying timeslots for a listing using the booking unit "fixed" or "hourly", the template uses [interval-based filtering](/references/availability/#interval-based-filtering). This approach allows retrieving significantly less data from the API than using regular timeslot queries (the API is restricted to 500 timeslots per page). A regular timeslot query can only fetch up to 500 time slots at once, and to fetch more than that you need to make multiple queries. When using short fixed time slots, e.g. 15 minutes, one day might have dozens of available time slots, so the first 500 time slots of a month might only cover a few weeks worth of availability. The date picker component needs to know whether there is availability on a certain day, so the template makes a query that matches the first available timeslot within the defined interval. See the full query in [ListingPage.duck.js](https://github.com/sharetribe/web-template/blob/main/src/containers/ListingPage/ListingPage.duck.js#L344) This information is used to display the availability of a listing in the monthly calendar view when booking: ![Availability calendar showing available dates for April](./april-availability.png) For listings using the fixed booking slot unit type, the template extends the end time of the timeslot query. If the duration of the fixed booking slot is e.g. 2 hours, the end time of the query is extended by 2 hours. This allows for the timeslot query to return availability correctly for the last moments of the month. For example, to display availability on the last day of the month at 23:55 for a 2 hour fixed booking slot, the template needs to check if there is availability 2 hours onwards from 23:55, therefore extending the query into the next month. See the [relevant code in ListingPage.duck.js](https://github.com/sharetribe/web-template/blob/main/src/containers/ListingPage/ListingPage.duck.js#L337-L342). Once the user selects a date from the calendar picker, the template makes a subsequent timeslot query to retrieve all available timeslots for the specific day, as the initial query only includes a subset of available timeslots. --- ## How to manage hosted and local configurations Path: template/configuration/hosted-and-local-configurations/index.mdx # How to manage hosted and local configurations The Sharetribe Web Template has a number of configurations that can be used to customise the template with low code effort. Starting in version v2.0.0, the template can also use [asset-based](/references/assets/) configurations, if they are defined in Sharetribe Console. The [Asset Delivery API](https://www.sharetribe.com/api-reference/asset-delivery-api.html) is used to fetch no-code content and configurations, defined in the Console, to your client application. [See all the available assets.](/references/assets/#available-default-asset-files) Sharetribe Web Template fetches mandatory assets by default. Some configurations are mandatory to define in Sharetribe Console, such as logo, listing types, listing fields, and minimum transaction size. Those configurations are read from the hosted assets in the template by default. This means that if you want to, for instance, add a listing type with a custom transaction process, or add a custom listing field in the template code, you will need to modify how configurations get merged in the template. ## How configurations are merged in the template The template uses a function [mergeConfig](https://github.com/sharetribe/web-template/blob/main/src/util/configHelpers.js#L667) in _src/util/configHelpers.js_ to handle the configurations. ```shell └── src └── util └── configHelpers.js ``` ```jsx export const mergeConfig = (configAsset = {}, defaultConfigs = {}) => { // defaultConfigs.listingMinimumPriceSubUnits is the backup for listing's minimum price const listingMinimumPriceSubUnits = getListingMinimumPrice(configAsset.transactionSize) || defaultConfigs.listingMinimumPriceSubUnits; return { // Use default configs as a starting point for app config. ...defaultConfigs, // Overwrite default configs if hosted config is available listingMinimumPriceSubUnits, // Analytics might come from hosted assets at some point. analytics: mergeAnalyticsConfig( configAsset.analytics, defaultConfigs.analytics ), // Branding configuration comes entirely from hosted assets, // but defaults to values set in defaultConfigs.branding for // marketplace color, logo, brandImage and Facebook and Twitter images branding: mergeBranding( configAsset.branding, defaultConfigs.branding ), // Layout configuration comes entirely from hosted assets, // but defaultConfigs is used if type of the hosted configs is unknown layout: mergeLayouts(configAsset.layout, defaultConfigs.layout), // Listing configuration comes entirely from hosted assets listing: mergeListingConfig(configAsset, defaultConfigs), // Hosted search configuration does not yet contain sortConfig search: mergeSearchConfig( configAsset.search, defaultConfigs.search ), // Map provider info might come from hosted assets. Other map configs come from defaultConfigs. maps: mergeMapConfig(configAsset.maps, defaultConfigs.maps), // Include hosted footer config, if it exists // Note: if footer asset is not set, Footer is not rendered. footer: configAsset.footer, // Check if all the mandatory info have been retrieved from hosted assets hasMandatoryConfigurations: hasMandatoryConfigs(configAsset), }; }; ``` Within the _mergeConfig_ function, each different configuration category has a separate merge function. For example, let's take a look at how to modify the _mergeListingConfig_ function to include listing types from both hosted asset based configurations and local configurations. ```jsx // Note: by default, listing types and fields are only merged if explicitly set for debugging const mergeListingConfig = (hostedConfig, defaultConfigs) => { // Listing configuration is splitted to several assets in Console const hostedListingTypes = restructureListingTypes( hostedConfig.listingTypes?.listingTypes ); const hostedListingFields = restructureListingFields( hostedConfig.listingFields?.listingFields ); // The default values for local debugging const { listingTypes: defaultListingTypes, listingFields: defaultListingFields, ...rest } = defaultConfigs.listing || {}; // When debugging, include default configs. // Otherwise, use listing types and fields from hosted assets. const shouldMerge = mergeDefaultTypesAndFieldsForDebugging(false); const listingTypes = shouldMerge ? union(hostedListingTypes, defaultListingTypes, 'listingType') : hostedListingTypes; const listingFields = shouldMerge ? union(hostedListingFields, defaultListingFields, 'key') : hostedListingFields; const listingTypesInUse = getListingTypeStringsInUse(listingTypes); return { ...rest, listingFields: validListingFields(listingFields, listingTypesInUse), listingTypes: validListingTypes(listingTypes), enforceValidListingType: defaultConfigs.listing.enforceValidListingType, }; }; ``` By default, this function first determines the hosted and local listing fields and listing types. Then, it checks whether to merge both, or to only use hosted configurations. When debugging your code, you can toggle the parameter for _mergeDefaultTypesAndFieldsForDebugging_ into _true_ to show both local and hosted configurations. ## Example: use both hosted and local configs for listing types Let's say you want to use multiple listing types in your marketplace – one for regular bookings, defined in Sharetribe Console, and one for [negotiated bookings](https://github.com/sharetribe/example-processes/tree/master/negotiated-booking), defined in the template. First, follow the directions in the process README.md to configure the template to use the new process. Then, make the following modifications to the _mergeListingConfig_ function: ```diff const mergeListingConfig = (hostedConfig, defaultConfigs) => { /* ... */ const shouldMerge = mergeDefaultTypesAndFieldsForDebugging(false); - const listingTypes = shouldMerge - ? union(hostedListingTypes, defaultListingTypes, 'listingType') - : hostedListingTypes; + const listingTypes = union(hostedListingTypes, defaultListingTypes, 'listingType'); const listingFields = shouldMerge ? union(hostedListingFields, defaultListingFields, 'key') : hostedListingFields; /* ... */ ``` With this change, you are still retrieving listing fields and other configurations primarily from hosted configurations, and using default configurations only as a fallback. However, listing types now combine both hosted asset and default config listing types. --- ## Currency configurations Path: template/configuration/how-to-set-up-currency-in-template/index.mdx # Currency configurations ## Change or override the marketplace currency Versions 3.4.0 and newer of the template fetch currency data through [assets](/references/assets/). You can change your marketplace currency in [Console > Build > General > Localization](https://console.sharetribe.com/a/general/localization). If you want to override the currency information fetched using assets, you can do so by editing the currency code in the [configDefault.js](https://github.com/sharetribe/web-template/blob/main/src/config/configDefault.js#L20) file. You will also need to update the mergeCurrency function in [configHelpers.js](https://github.com/sharetribe/web-template/blob/main/src/util/configHelpers.js#L51-L63) to prevent the template from using the currency defined in assets. ```diff const mergeCurrency = (hostedCurrency, defaultCurrency) => { - const currency = hostedCurrency || defaultCurrency; + const currency = defaultCurrency; const supportedCurrencies = Object.keys(subUnitDivisors); if (supportedCurrencies.includes(currency)) { return currency; } else { console.error( `The given currency (${currency}) is not supported. There's a missing entry on subUnitDivisors` ); return null; } }; ``` ## Supported currencies Stripe is the default payment processor in Sharetribe. If you want to support payments using the default payment integration, please confirm that [Stripe supports the currency](https://stripe.com/docs/currencies) you intend to use. If you configure your marketplace in a currency not supported by Stripe, you can still use transaction processes without payments. ## Currency codes The currency configuration must be in the [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes), e.g. USD, EUR, CAD, AUD, etc. The default value is USD. See a full list of currency codes of all currencies in use in the [settingsCurrency.js](https://github.com/sharetribe/web-template/blob/f8e1ceff83f06ce62c94a66ef0b8236fa2c5c218/src/config/settingsCurrency.js#L3) file. ## Changing listing minimum price If you are using the newest version of The Sharetribe Web Template, you can edit your listing minimum price through [Console](https://console.sharetribe.com/a/transactions/minimum-transaction-size). You need to specify the minimum price as the subunits of the currency you are using, i.e. if you are using dollars, 500 would set the minimum price for a listing at 5 dollars. You can also set the value as 0, meaning there is no minimum price. We recommend that the minimum listing price be at least the same as [Stripe's minimum charge amount](https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts) in the country you are operating. **If the listing price is lower than Stripe's minimum charge amount, Stripe will not process the payment and the transaction will fail.** If you're not using the newest version of The Sharetribe Web Template, or if you prefer to use local configuration files you can adjust the `listingMinimumPriceSubUnits` variable in [configDefault.js](https://github.com/sharetribe/web-template/blob/main/src/config/configDefault.js#L25). The variable `listingMinimumPriceSubUnits` defines the minimum price a user can give a listing. If you want to allow listings with no price, you will need to [add a new transaction process](/how-to/transaction-process/change-transaction-process-in-template/) where you have removed pricing and payment related actions. Alternatively, you can use a transaction process without payments, such as the inquiry process. ## Currency subunits The [settingsCurrency.js](https://github.com/sharetribe/web-template/blob/main/src/config/settingsCurrency.js) file specifies an array of currency sub-units the template uses to format currencies correctly. If the currency is a [zero-decimal currency](https://stripe.com/docs/currencies#zero-decimal) (e.g. JPY) it uses value 1. The template only supports currencies with subunit divisors of 100 or smaller. While currencies with larger subunit divisors (e.g., 1000, such as the Iraqi Dinar) could be supported, they are not currently compatible with the existing email templates, which assume a divisor of 100. ### Why subunits? All API requests to Stripe expects amounts to be provided in a currency’s smallest unit. It's also better to calculate using integers than floats to avoid rounding errors. ## Formatting currency Formatting money is done by using [React Intl](https://github.com/yahoo/react-intl). The component [`FieldCurrencyInput`](https://github.com/sharetribe/web-template/blob/main/src/components/FieldCurrencyInput/FieldCurrencyInput.js) converts user input to a formatted message and adds the Money object to the price attribute of a listing. All currency is formatted specified by the value set in the [configDefault.js](https://github.com/sharetribe/web-template/blob/main/src/config/configDefault.js#L20) file. ## Calculating price amounts client-side If you need to calculate the price on client app side use [Decimal.js](https://github.com/MikeMcl/decimal.js/) library. Currently, there are two places in the template where prices are calculated: - [server/api-util/lineItemHelpers.js](https://github.com/sharetribe/web-template/blob/main/server/api-util/lineItemHelpers.js) - [EstimatedCustomerBreakdownMaybe.js](https://github.com/sharetribe/web-template/blob/main/src/components/OrderPanel/EstimatedCustomerBreakdownMaybe.js) ## Supporting multiple currencies The template is designed to be currency-agnostic, making it technically possible to support multiple currencies. Pricing information is associated with individual listings, allowing for potentially having multiple listings in different currencies. However, this functionality is not enabled by default, as multi-currency support presents several challenges. If you are interested in building multi-currency support, consider the following: 1. Listings can have only one `price` attribute ([see API reference](https://www.sharetribe.com/api-reference/marketplace.html#listing-resource-format)). Storing additional currency information requires creating a duplicate of each listing in each supported currency, or utilising extended data to store additional currency and pricing information. 2. Price filtering is based on amount only and does not take the currency into account. This can lead into problems if you want to compare prices. You might have dollars and yens in the same marketplace. One USD is 110 JPY, so the default price filtering will not easily find equally priced items if they are in different currencies. 3. Currency conversions may cause complexity. If most of your users have a credit card in one currency but listings are in a different currency, some conversion costs will take effect. --- ## Template environment variables Path: template/configuration/template-env/index.mdx # Template environment variables You can change the following environment variables to specify API credentials or enable certain functionalities. Most have a default value that allows you to run the template locally. However, when you are ready to deploy your marketplace to production, you should reference this list of environment variables. ## List of available environment variables - **`REACT_APP_MAPBOX_ACCESS_TOKEN`** See the [How to set up Mapbox](https://www.sharetribe.com/help/en/articles/8676185-how-to-set-up-mapbox-or-google-maps-for-location-services) guide for more information. - **`REACT_APP_GOOGLE_MAPS_API_KEY`** See the [How to use Google Maps](https://www.sharetribe.com/help/en/articles/8676185-how-to-set-up-mapbox-or-google-maps-for-location-services) guide for more information. - **`REACT_APP_SHARETRIBE_SDK_CLIENT_ID`** Your application's client ID. You can get this from [Sharetribe Console](https://console.sharetribe.com/advanced/applications). - **`SHARETRIBE_SDK_CLIENT_SECRET`** Your application's client secret. It's related to client ID and used for privileged transitions from server side. You can get this from [Sharetribe Console](https://console.sharetribe.com/advanced/applications). - **`REACT_APP_STRIPE_PUBLISHABLE_KEY`** Stripe publishable API key for generating tokens with Stripe API. Use test key (prefix `pk_test_`) for development. The secret key needs to be added to Sharetribe Console. - **`REACT_APP_MARKETPLACE_ROOT_URL`** The root url of the marketplace. Needed for social media sharing, SEO optimization, and social logins. Note that the value should not include a trailing slash. - **`REACT_APP_MARKETPLACE_NAME`** Marketplace name in self-hosted marketplaces is set through environment variables. If not set, this defaults to 'Biketribe' in src/config/configDefault.js. - **`NODE_ENV`** Node env. Use 'development' for development and 'production' for production. - **`PORT`** Port for server to accept connections. - **`REACT_APP_ENV`** A more fine grained env definition than NODE_ENV. Is used for example to differentiate envs in logging. - **`REACT_APP_SHARETRIBE_USING_SSL`** Redirect HTTP to HTTPS. - **`SERVER_SHARETRIBE_TRUST_PROXY`** Set when running the app behind a reverse proxy, e.g. in Heroku or Render. - **`REACT_APP_SENTRY_DSN`** See the [How to set up Sentry to log errors](/template/testing/how-to-set-up-sentry/) guide for more information. - **`REACT_APP_CSP`** See the [How to set up Content Security Policy (CSP)](/template/security/how-to-set-up-csp-for-template/) guide for more information. - **`BASIC_AUTH_USERNAME`** Set to enable HTTP Basic Auth. - **`BASIC_AUTH_PASSWORD`** Set to enable HTTP Basic Auth. - **`REACT_APP_GOOGLE_ANALYTICS_ID`** See the [How to set up Analytics](/template/analytics/how-to-set-up-analytics-for-template/) guide for more information. - **`REACT_APP_PLAUSIBLE_DOMAINS`** Used to configure Plausible Analytics. Read more in [how to set up analytics](/template/analytics/how-to-set-up-analytics-for-template/). * **`REACT_APP_SHARETRIBE_SDK_BASE_URL`** The base url to access the Sharetribe Marketplace API. The template uses the correct one by default so no need to set this. * **`REACT_APP_FACEBOOK_APP_ID`** App ID of a Facebook App when Facebook login is used. Adding this key does not yet enable the feature. See [how to enable Facebook login](/how-to/users-and-authentication/enable-facebook-login/). * **`FACEBOOK_APP_SECRET`** App secret of a Facebook App when Facebook login is used. * **`REACT_APP_GOOGLE_CLIENT_ID`** The Client ID of your Google Sign-In. Corresponds to client ID of the identity provider client in Console. Adding this key does not yet enable the feature. See [how to enable Google login](/how-to/users-and-authentication/enable-google-login/). * **`GOOGLE_CLIENT_SECRET`** The Client Secret of your Google Sign-In. * **`REACT_APP_SHARETRIBE_SDK_ASSET_CDN_BASE_URL`** Used to initialize the SDK with a custom base URL. Only use this if you want to proxy asset SDK calls through your server. The template uses the correct base URL by default if left empty. The Sharetribe Template webpack configuration is based on the now deprecated Create React App (CRA). The template uses Webpack's DevServer for providing a Hot Module Reloading feature. In some environments, Webpack DevServer might require configuring additional environment variables. You can also check out [CRA's advanced configurations](https://create-react-app.dev/docs/advanced-configuration) if you experience problems with socket ports. Most of these advanced configurations are still used in the template, but if you're wondering whether a specific variable is related to your issues, we recommend making a codebase search for the variable to see whether it is being used in the `/config` or `/scripts` directories. ## Setting environment variables When the app is started locally with `yarn run dev` or `yarn run dev-server`, you can set environment variables by using the (gitignored) `.env` file. You can edit the basic variables via `yarn run config` or by editing directly the `.env` file. Some variables can be edited only in the .env file. The repository contains a template file `.env-template` with default configuration. The template also has an .env.development file, used when you run `yarn run dev` or `yarn run dev-server`, and .env.test file, used when you run `yarn run test`. These files are used for context-specific overrides, such as setting REACT_APP_ENV. In production, it's recommended that you set the configuration via env variables and do not deploy an `.env` file. The client application will only be packaged with env variables that start with `REACT_APP`. This way server secrets don't end up in client bundles. Only use the REACT_APP prefix with environment variables that you want to reveal to the public internet. It is important to **never** publicly reveal the client secret of the Marketplace or Integration API – in other words, DO NOT prefix those variables with REACT_APP! Environment variables are bundled with the client during build time. If you change environment variables locally or in production, you must remember to rebuild the client bundle. In production, this means redeploying the application. Locally, you need to rerun either `yarn run dev` or `yarn run dev-server`. --- ## Configuration variables Path: template/configuration/variables/index.mdx # Configuration variables There are a lot of settings you can edit and configure through configuration files. These settings are related to branding, layout, listings, search, maps, payment and transactions. You can find all configuration files in the [config directory](https://github.com/sharetribe/web-template/tree/main/src/config). ## Using Console for configurations Console allows users to configure many configuration variables through a no-code interface. The newest release of The Sharetribe Web Template supports fetching the configuration data through the [Asset Delivery API](https://www.sharetribe.com/api-reference/asset-delivery-api.html). If you are using an older version of the web-template, follow the instructions [here](/template/introduction/how-to-customize-template/#pull-in-the-latest-upstream-changes) to pull in the latest upstream changes. Settings configured in local configurations files are overridden by any fetched via the Asset Delivery API. You can refer to [this article](/template/configuration/hosted-and-local-configurations/) to modify the way your template merges local and hosted configurations. ## Configuration files All relevant configuration is split between the following files and the environment variables. You can see the available environment variables in the [template environment variables](/template/configuration/template-env/) article. - [configBranding.js](https://github.com/sharetribe/web-template/blob/main/src/config/configBranding.js) - Marketplace colour, logos, Facebook and Twitter media - [configDefault.js](https://github.com/sharetribe/web-template/blob/main/src/config/configDefault.js) - Change localization settings, marketplace currency and add social media links - [configLayout.js](https://github.com/sharetribe/web-template/blob/main/src/config/configBranding.js) - Layout or the search and listing page and aspect ratio for listing image variants - [configListing.js](https://github.com/sharetribe/web-template/blob/main/src/config/configListing.js) - You can define extended data variables and how they are displayed on both the listing and search page. Read more about the available settings in the [configListing.js](https://github.com/sharetribe/web-template/blob/main/src/config/configListing.js) file. - [configSearch.js](https://github.com/sharetribe/web-template/blob/main/src/config/configSearch.js) - Change between keyword or location search and set default search filters and sorting options - [configMaps.js](https://github.com/sharetribe/web-template/blob/main/src/config/configMaps.js) - Choose to load maps through Google or Mapbox and location-related search settings - [configStripe.js](https://github.com/sharetribe/web-template/blob/main/src/config/configStripe.js) - The configuration that Stripe needs to function correctly - [configDefaultLocationSearches.js](https://github.com/sharetribe/web-template/blob/main/src/config/configDefaultLocationSearches.js) - Specify a list of locations that are shown to the user as suggestions when they click on the search bar ## Branding The [configBranding.js](https://github.com/sharetribe/web-template/blob/main/src/config/configBranding.js) file allows you to customize the branding and appearance of your marketplace. The configurations you can change include the marketplace's color, logos for desktop and mobile devices, a background image, and default images for social media sharing. The `marketplaceColor` constant defines the primary color for your marketplace, which is used to style various elements throughout the site. The `logoImageDesktopURL` and `logoImageMobileURL` constants specify the URL for the desktop and mobile logos. The `brandImageURL` constant specifies the URL for a background image that is used on several pages. The `facebookImageURL` and `twitterImageURL` constants specify the default images for social media sharing on Facebook and Twitter. ## Common configurations You can find all common configurations in the [configDefault.js](https://github.com/sharetribe/web-template/blob/main/src/config/configDefault.js) file. The `marketplaceName` setting specifies the name of your marketplace. This name is used in various places throughout the site, such as in marketplace texts and in meta tags for SEO and social media sharing. ### Currency and pricing The currency setting specifies the currency used in your marketplace. It must be in ISO 4217 currency code and should match one of the currencies listed in the [settingsCurrency.js](https://github.com/sharetribe/web-template/blob/main/src/config/settingsCurrency.js) file. The `listingMinimumPriceSubUnits` setting specifies the minimum price for a listing in your marketplace, expressed in currency subunits (e.g., cents). A value of 0 means that there is no minimum price for listings. Note that Stripe may charge a minimum fee that depends on factors such as the country and currency. You can read more about currency configurations in the [currency configurations article](/template/configuration/how-to-set-up-currency-in-template/). ### Locale You can use the localization setting to specify the locale and the first day of the week used in calendars. This setting contains two properties: - `locale`: The locale of your marketplace, expressed as a language code. The default value is 'en' for English. - `firstDayOfWeek`: The first day of the week in your marketplace, expressed as a number between `0` (Sunday) and `6` (Saturday). The default value is `1` for Monday. For example, to change the locale to French, you would set the `locale` property to `'fr'`. To change the first day of the week to Sunday, you would set the `firstDayOfWeek` property to `0`. ### Structured data The `siteFacebookPage`, `siteInstagramPage`, and `siteTwitterHandle` settings in the code specify the [Schema.org](https://schema.org/) organization metadata and are used in the meta tags for social media sharing. The `siteFacebookPage`, `siteInstagramPage` and `siteTwitterHandle` settings specify the social media pages associated with your marketplace or organization. With the two first options, the pages should be expressed as an URL (e.g., 'https://www.facebook.com/sharetribe/'). For the `siteTwitterHandle` option, you should use a username (e.g., '@sharetribe'). You can also specify address information to be used in your site's structured data. The address setting contains four properties: - `addressCountry`: The country in which your marketplace or organization is located, expressed as an ISO 3166-1 alpha-2 country code (e.g., 'FI' for Finland). - `addressRegion`: The region or state in which your marketplace or organization is located (e.g., 'Helsinki'). - `postalCode`: The postal code of the location of your marketplace or organization (e.g., '00130'). - `streetAddress`: The street address of the location of your marketplace or organization (e.g., 'Erottajankatu 19 B'). ## Layout configuration There are three layout options that you can toggle to change how the search page is rendered and how listing images are displayed in your marketplace. You can find all layout options in the [configLayout.js](https://github.com/sharetribe/web-template/blob/main/src/config/configLayout.js) file. ### Search page The SearchPage component of the template has two layout variants: 'map' and 'grid'. ```js // There are 2 SearchPage variants that can be used: // 'map' & 'grid' export const searchPage = { variantType: 'map', }; ``` You can change the layout of the search page in the [configLayout.js](https://github.com/sharetribe/web-template/blob/main/src/config/configLayout.js#L9) file by toggling the `variantType` variable between 'map' and 'grid'. The 'grid' layout does not contain a map but instead, displays a grid of listings with filters shown in the sidebar. This layout is best suited for cases where users are interested in browsing a list of search results rather than selecting a location on a map. The 'map' variant of the SearchPage component displays a map next to search results. You can use the map to select a location to search from, allowing users to browse listings in a specific area easily. This layout is ideal for cases where users are interested in searching for listings within a particular location or neighborhood. ### Listing images You can toggle between two options on how listing images are displayed on the listing page. The 'coverPhoto' layout option shows a hero section with a cropped image at the beginning of the page. The 'carousel' layout option displays an image carousel, which renders images using their original aspect ratio. You can change the layout of the search page in the [configLayout.js](https://github.com/sharetribe/web-template/blob/main/src/config/configLayout.js#L16) file by toggling the `listingPage` variable between 'coverPhoto' and 'carousel'. ```js export const listingPage = { variantType: 'carousel', }; ``` ### Listing image aspect ratio Use the `listingImage` option to specify the aspect ratio and image variants for listing images in your marketplace. The option defines the aspect ratio of the listing image everywhere except on the listing page. The aspect ratio of the image on the listing page can be defined by toggling the listingPage option between 'coverPhoto' and 'carousel' (the carousel option will display images in their original aspect ratio). For example, to change the aspect ratio of the images to 3:1, you would set the aspectRatio property to 3/1: ```js export const listingImage = { variantType: 'cropImage', // Aspect ratio for listing image variants (width/height) // Note: This will be converted to separate aspectWidth and aspectHeight values // to make calculations easier. aspectRatio: '3/1', // Listings have custom image variants, which are named here. variantPrefix: 'listing-card', }; ``` Which would look like this on the search page: ![Image of aspect ratio](./aspect-ratio.png) ## Listing configuration ### Extended data configuration The `listingFields` is an array of configuration options for extended data fields. [Extended data](/concepts/extended-data/extended-data-introduction/) fields are additional pieces of information that can be added to a listing. Each object in the array represents a single extended data field. You can find the full list of configuration options for extended data fields in the [configListing.js](https://github.com/sharetribe/web-template/blob/main/src/config/configListing.js#L11) file. Adding a new entry to the `listingFields` array will automatically add a new input field to the listing creation wizard. Say we add a new extended data field using the following options: ```js { key: 'frame', scope: 'public', schemaType: 'enum', enumOptions: [ { option: 'aluminium', label: 'Aluminium' }, { option: 'steel', label: 'Steel' }, { option: 'titanium', label: 'Titanium' }, ], filterConfig: { showFilter: false, filterType: 'SelectSingleFilter', label: 'Frame material', group: 'primary', }, showConfig: { label: 'Frame material', isDetail: true, }, saveConfig: { label: 'Frame material', placeholderMessage: 'Select frame material', isRequired: false, }, }, ``` When creating a new listing, we will see the new input field pop up: ![Image of input field](./inputfield.png) And while the `isDetail` value in the `showConfig` object is toggled to `true`, the extended data attribute will also be listed on the listing page: ![Image of input field](./description.png) We will also be able to see the new extended data field on the search page and use it to filter listings. If we change the `showFilter` value to `true`, we will see a new filter component pop up, that can be used to filter listings by frame material: ![Filter component to filter listings with frame material](./filtercomponent.png) If you do enable the `showFilter` variable, you must also set a search schema. Without setting a search schema for the extended data, the filter component will not work. Learn how to set a search schema for a extended data attribute in the [manage search schemas article](/how-to/search/manage-search-schemas-with-sharetribe-cli/). In the current version of the template, search filters show up automatically for **enum**, **multi-enum**, and **long** schema types. For schema type **boolean**, you will need to create custom filtering options. Schema types **text** and **shortText** are included in keyword search. ### Listing type configurations The [configListing.js](https://github.com/sharetribe/web-template/blob/main/src/config/configListing.js) file contains an array of listing type and their associated transaction process configurations. Listing types contain information on the transaction process the listing uses and what unit of time is used to book the listing (e.g. daily or hourly). When creating a listing, the user can select the listing type from a dropdown based on the listing types defined in the configuration files. The listing type and transaction process alias are stored in the public data of the listing. Listing types can also be used to define whether listings of the type should show available stock. You can use this configuration to enable different listing types, either using the same transaction processes or different ones. Each listing type can have only one transaction process. However, since you can have multiple listing types per marketplace, you can also have multiple transaction processes in use at one time. - Read more: [Change transaction process in Sharetribe Web Template](/how-to/transaction-process/change-transaction-process-in-template/). ## Search configuration The [configSearch.js](https://github.com/sharetribe/web-template/blob/main/src/config/configSearch.js) file allows you to adjust the default search filters (price and dates) and how listings are sorted on the search page. ### Search filters In the configSearch.js file you can change if the search bar supports location or keyword search by toggling the `mainSearch` variable between 'keywords' and 'location'. This file allows you to configure or remove the dates and price filter. To remove the filters, comment them out of the `defaultFilters` array. You can adjust two variables within the date filter: `entireRangeAvailable` and `mode`. The `entireRangeAvailable` config is _true_ by default. When it is true, filtering with a time range (e.g. May 1st-May 7th) only returns listings with availability for the entire duration. If `entireRangeAvailable` is _false_, filtering with the same dates returns listings with at least some availability between the start and end dates. The `mode` config can be assigned to either `day` or `night`. Using the value `day` will allow your users to select a single day through the datepicker element. ### Sorting Operators can configure which sort options appear in the SortBy dropdown directly in Console, toggling individual options on or off without any code changes. These Console-managed settings are fetched via the Asset Delivery API and take precedence over local configuration. The `sortConfig` in [configSearch.js](https://github.com/sharetribe/web-template/blob/main/src/config/configSearch.js) defines the default sort options used as a fallback when no hosted configuration is present. You can also disable the sorting element altogether, or add sort options based on extended data through this local config. See all the available sorting options in the [API reference](https://www.sharetribe.com/api-reference/marketplace.html#sorting). If you want to customize how hosted and local sort configurations are merged, refer to the [hosted and local configurations](/template/configuration/hosted-and-local-configurations/) article. ## Map configurations The [configMaps.js](https://github.com/sharetribe/web-template/blob/main/src/config/configMaps.js) file allows you to set up a map provider and adjust map-related settings. See [this article](/template/maps/configure-maps/) for a complete overview of what map-related adjustments you can achieve through the configuration files. If you are trying to change the map provider, see the [how to set up Google Maps or Mapbox](https://www.sharetribe.com/help/en/articles/8676185-how-to-set-up-mapbox-or-google-maps-for-location-services) article. ## Stripe and transactions ### Stripe configurations The [configStripe.js](https://github.com/sharetribe/web-template/blob/main/src/config/configStripe.js) file includes all countries supported by the Sharetribe Stripe integration. The list of countries is used during the Stripe onboarding process. In most cases, no changes are required to this file. --- ## Hosted marketplace texts with Asset Delivery API Path: template/content-management/hosted-marketplace-texts/index.mdx # Hosted marketplace texts with Asset Delivery API Marketplace texts can be managed both in the built-in marketplace text files and in Sharetribe Console. This article describes how the template uses the hosted marketplace texts and merges them with the built-in marketplace texts. ## Hosted marketplace texts **Hosted marketplace texts** refer to the texts that the marketplace operator can edit in Sharetribe Console. The client app then needs to fetch it using the Asset Delivery API. It is good to note that even if the operator adds some hosted marketplace texts using Sharetribe Console, the template still needs to have a built-in marketplace text file for the marketplace text keys that do not have a value in the hosted asset. That way, the UI can still render something meaningful for the parts of the page that the operator has not modified. The template specifies the path to the hosted marketplace texts as part of the app-wide configuration in [_config/configDefault.js_](https://github.com/sharetribe/web-template/blob/main/src/config/configDefault.js#L89). These hosted marketplace texts live in a file called _content/translations.js_, since language-specific marketplace text files make it fairly easy to translate the template to languages other than the default English. ```jsx filename="config/configDefault.js" showLineNumbers{89} appCdnAssets: { translations: '/content/translations.json', footer: '/content/footer.json', topbar: '/content/top-bar.json', branding: '/design/branding.json', layout: '/design/layout.json', localization: '/general/localization.json', accessControl: '/general/access-control.json', userTypes: '/users/user-types.json', userFields: '/users/user-fields.json', categories: '/listings/listing-categories.json', listingTypes: '/listings/listing-types.json', listingFields: '/listings/listing-fields.json', search: '/listings/listing-search.json', transactionSize: '/transactions/minimum-transaction-size.json', analytics: '/integrations/analytics.json', googleSearchConsole: '/integrations/google-search-console.json', maps: '/integrations/map.json', }, ``` In addition, the template has a global Redux file (_src/ducks/hostedAssets.duck.js_), which exports a Redux Thunk function called **fetchAppAssets**. This is the function that actually makes the calls to the Asset Delivery API. There are two ways to fetch marketplace text assets using Asset Delivery API: by version or by alias. #### Fetching marketplace texts by version All assets are identifiable by their version, and versions are immutable. Therefore if you fetch assets by version, they can be cached for an extended period of time. Read more about [caching assets](/references/assets/#asset-data-caching). When fetching marketplace texts by version, the result is cached for an extended period of time, which helps to avoid unnecessary data loading. Since Asset Delivery API sets Cache-Control header for these responses, the browser knows to cache these responses on its own. Hosted assets are versioned as a [whole asset tree](/references/assets/#asset-versioning) - a bit similar to how Git works. Individual asset files might have not changed when the whole version has changed and this might cause [HTTP redirects](https://www.sharetribe.com/api-reference/asset-delivery-api.html#http-redirects). Since the template uses Sharetribe SDK, the response always contains the data for the requested asset, even if the asset has not changed in the specified version. ```jsx sdk.assetByVersion({ path: 'content/translations.json', version: '', }); ``` #### Fetching marketplace texts by alias In addition to fetching assets by version, you can fetch them by a specific alias instead of a version. Currently, the marketplace text asset can be fetched with the alias _latest_, which returns the most recently updated version of the marketplace text file. The response also contains the version information for the most recent asset, so that subsequent fetches can be done based on asset version. When fetching by alias, the cache time is a few seconds for the Dev and Test environments and up to 5 minutes for the Live environment. In other words, it can take up to 5 minutes for marketplace text updates to be visible in a Live environment. These cache times are subject to change. ```js sdk.assetByAlias({ path: 'content/translations.json', alias: 'latest', }); ``` ## How production build works with hosted marketplace texts This setup is in use when the server is running, i.e. when you run `yarn start` in your host environment or `yarn run dev-server` on your local machine. {

} Export `fetchAppAssets` from _src/index.js_ to make it available for the server. {

} The server initialises the store using `fetchAppAssets`: ```jsx filename="server/dataLoader.js" const store = configureStore({ initialState: {}, sdk }); return store.dispatch(fetchAppAssets(defaultConfig.appCdnAssets)); ``` Here the `fetchAppAssets` thunk fetches the latest version of `content/translations.json` asset by using _latest_ alias with `sdk.assetByAlias`. This ensures that the server-side rendering has the most recent version of asset to render the page {

} A `loadData` call is made if the route specifies it: ```jsx filename="server/dataLoader.js" .then(fetchedAppAssets => { const { translations: translationsRaw, ...rest } = fetchedAppAssets || {}; translations = translationsRaw?.data || {}; hostedConfig = { ...hostedConfig, ...extractHostedConfig(rest) }; return Promise.all(dataLoadingCalls(hostedConfig)); ``` {

} An asset **version** is saved to the store and passed to the browser through `preloadedState`: ```jsx filename="server/dataLoader.js" .then(() => { return { preloadedState: store.getState(), translations, hostedConfig }; }) ``` - Server-side rendering must match with client-side rendering when the browser hydrates the server-side rendered content. - To ensure that client-side rendering has the same version of marketplace texts, the asset version is passed (through preloaded state) to front end. {' '} The template does not pass the marketplace text asset itself from the server to browser. The reason is that if browser fetches the versioned asset file directly, it can leverage browser's cache. So, every page load, after the initial one, will use the _translation.json_ file from the browser's local cache. {

} The app calls the **renderApp** function, which renders `` in _src/app.js_. The hosted marketplace text file from Asset Delivery API is passed as a prop to ``: ```jsx filename="server/renderer.js" // Render the app with given route, preloaded state, hosted microcopy. // Render the app with given route, preloaded state, hosted translations. return renderApp( requestUrl, context, preloadedState, translations, hostedConfig, collectWebChunks ).then(({ head, body }) => { ``` ```jsx filename="src/app.js" ``` {

} Hosted marketplace texts are then merged with the default marketplace texts in the _ServerApp_ component. ```jsx filename="src/app.js" {20} export const ServerApp = props => { const { url, context, helmetContext, store, hostedTranslations = {}, hostedConfig = {} } = props; const appConfig = mergeConfig(hostedConfig, defaultConfig); HelmetProvider.canUseDOM = false; // Show MaintenanceMode if the mandatory configurations are not available if (!appConfig.hasMandatoryConfigurations) { return ( ); } return ( ``` {

} The browser initializes the store with a preloaded state. The asset version that the SSR used, is included in that preloaded state. {

} The browser uses the `fetchAppAssets` thunk to fetch assets by version, using the `sdk.assetByVersion` method. {

} The browser loads the initial data using a load data call. {

} Hydrate the `` and pass marketplace texts as props. Hosted marketplace texts are merged with default marketplace texts in the ClientApp component. ```jsx filename="src/index.js" showLineNumbers{90} ``` ## How development build works with hosted marketplace texts This is setup is in use if you run `yarn run dev` on your local machine. {

} The browser initialises the store. {

} The browser uses the **fetchAppAssets** thunk to fetch assets using _latest_ alias, calling **sdk.assetByAlias**. As the server is not available to fetch the latest version of the asset files, this call needs to be made from browser. {

} The browser makes a **loadData** call. {

} The browser renders the `` and passes marketplace texts as props. ## Read more If you want to read more, here are some pointers: - [Asset Delivery API](/references/assets/) - [Short intro to SSR](/template/routing/how-routing-works-in-template/#a-brief-introduction-to-ssr) --- ## How to add a static page Path: template/content-management/how-to-add-static-pages/index.mdx # How to add a static page You can create new content pages through the Console using the Pages feature. However, sometimes you may need a static page that does not use dynamic content. This guide walks you through the steps required to create a static and non-dynamic content page, from where to create the new component to how to add new routes. You might want to do this, for instance, if - you have a page that embeds a component that fetches its own data, or - you want to optimise some pages for performance, so that they don't fetch any data. If you do not have a specific reason to create a static page directly into the codebase, we recommend that you create your content pages using [Pages](/concepts/content-management/content-management-in-sharetribe/). With Pages, you can create multiple content pages in Sharetribe Console and render them all using [the same components in Sharetribe Web Template](/template/content-management/page-builder/). ## Create a new folder Create a new folder under _src/containers/_ with the name of your static page. E.g. if you are creating a page for embedding your social media feeds, it should be named **SocialMediaPage**. Create a new JavaScript file using the folder name. The path should look like _src/containers/SocialMediaPage/SocialMediaPage.js_. Create a new CSS file using the folder name. The path should look like _src/containers/SocialMediaPage/SocialMediaPage.module.css_. ## Create the component Template for a single column static page (SocialMediaPage.js): (We'll go through this line-by-line below.) ```jsx NamedLink, ExternalLink, LayoutComposer, Heading, } from '../../components'; const SocialMediaPage = () => { const layoutAreas = ` topbar main footer `; return ( {() => ( <>
{/* */} See Biketribe in Social Media Go to our home page or Go to Google
)} ); }; export default SocialMediaPage; ``` We are using [React](https://reactjs.org/) and [JSX](https://reactjs.org/docs/introducing-jsx.html) to create components and pages. Therefore, we need to import React to our new component which is done in the first line. ```jsx ``` ### Import components On the second line, we import some components: - **NamedLink** makes it easier to point to different pages inside the application - **ExternalLink** can be used to link outside the application. It creates a normal ``link with extra attributes `target="_blank" rel="noopener noreferrer"` that add some security to these outbound links. - **LayoutComposer** wraps the page layout elements by creating container and area wrappers using CSS Grid Template Areas. - **Heading** makes it easier to create headings with uniform style across the site. ```jsx NamedLink, ExternalLink, LayoutComposer, Heading, } from '../../components'; ``` After that we import three containers: - **StaticPage**: helps in creating static pages - **TopbarContainer**: creates our Topbar component and fetches the data it needs. - **FooterContainer**: creates our Footer component and fetches the data it needs. ```jsx ``` Then we need to import styles and possible other files from current folder. The template uses [CSS Modules](https://github.com/css-modules/css-modules) to scope all class names locally to prevent conflicts. ```jsx ``` After all the imports, we are finally getting into phase were we define the component. `const SocialMediaPage = props => { ... return ()}` defines a component called SocialMediaPage with content defined in return part. This is a [functional component](https://reactjs.org/docs/components-and-props.html). ### Define layout areas First, we need to define layout areas that will be used in the **LayoutComposer** component. Since we will pass a topbar, a main component area, and a footer, let's name those areas accordingly. ```jsx const layoutAreas = ` topbar main footer `; ``` ### Add page schema In the template above, we use StaticPage component with some attributes: ```jsx ``` - `className` is JSX name for `class` attribute used in plain HTML. - `title="Social media"` creates `Social media` element to `` section of the page. (That title is also used in OpenGraph meta tags). You could also add `description="This is the description for the social media page"` - Then we have `schema` tag that defines some data for search engines in JSON-LD format. Check [schema.org](https://schema.org/docs/full.html) for more information. You can also review [Google's structured data types](https://developers.google.com/search/docs/appearance/structured-data/search-gallery) to see if one of them fits your use case. ### Define component structure Inside the **StaticPage** component we wrap the content into the LayoutComposer component, which expects a functional React component as children. Within that component, we pass three elements that correspond to the layout areas defined earlier: - **TopBarContainer** for the _topbar_ layout area, - **div** with class _css.content_ for the main layout area, and - **FooterContainer** for the _footer_ layout area. Any static content we want to show on this page will be wrapped inside the _css.content_ **div**. ```jsx {() => ( <>
{/* */} See Biketribe in Social Media Go to our home page or Go to Google
)} ``` And as a final step we need to export the component. `export default SocialMediaPage;`. See more in [babeljs.org](https://babeljs.io/docs/en/learn/#modules) ## Style the component Here's an example what your _SocialMediaPage.module.css_ file could look like: ```css /** * Import custom media queries for the new page. * The template uses route-based code-splitting, every page create their own CSS files. * This import ensures that the page and components inside will get correct media queries, * when the app is build. */ @import '../../styles/customMediaQueries.css'; .content { margin: 0 auto; max-width: 784px; } .link { display: block; margin-top: 6px; margin-bottom: 6px; } ``` ## Add routing As a last step, you need to add the newly created static page to the routing. This can be done in _src/routing/routeConfiguration.js_. Inside the _routeConfiguration_ function, you should add a URL path, a page name (it should not conflicting with other pages), and the component itself. Add a new asynchronous import for the page in the beginning of the file with other page imports: ```js const SocialMediaPage = loadable( () => import( /* webpackChunkName: "SocialMediaPage" */ '../containers/SocialMediaPage/SocialMediaPage' ) ); ``` After that, add the route configuration to your newly created page: (In this example we created a social media page so '/social-media' would work well as a path.) ```javascript { path: '/social-media', name: 'SocialMediaPage', component: SocialMediaPage, }, ``` ## Read more We are using several libraries in this example. If you want to read more, here's some pointers: - [ES2015](https://babeljs.io/docs/en/learn/): imports, exports, arrow functions - [React](https://reactjs.org/): for creating components - [JSX](https://reactjs.org/docs/introducing-jsx.html): for getting HTML-like markup syntax for own components - [CSS Modules](https://github.com/css-modules/css-modules) - [React Router](https://reacttraining.com/react-router/web/guides/philosophy): routing inside the application. --- ## Bundled marketplace texts Path: template/content-management/how-to-change-bundled-marketplace-texts/index.mdx # Bundled marketplace texts The Sharetribe Web Template supports having a single language for the UI. Supported languages are English, French and Spanish, English being used by default. For information about changing the language, see the [Changing the language](/template/content-management/how-to-change-template-language/) article. We use the [React Intl](https://github.com/yahoo/react-intl) library to represent UI marketplace texts and to format dates, numbers, and money values. In addition to bundled marketplace texts, Sharetribe allows modifying marketplace texts through the Sharetribe Console: - [Marketplace texts in Sharetribe](/concepts/content-management/marketplace-texts/) - [How hosted marketplace texts work in the template](/template/content-management/hosted-marketplace-texts/) ## The marketplace text file All the bundled marketplace texts can be found in the [src/translations/en.json](https://github.com/sharetribe/web-template/blob/master/src/translations/en.json) file. The marketplace text data is formatted as one JSON object with all the marketplace text key-value pairs as properties. The key - value syntax is as follows: ```json ".": "" ``` For example: ```json "ManageListingCard.viewListing": "View listing" ``` The keys are namespaced to the corresponding component. This is aligned with the component driven paradigm that the application follows. It might introduce duplication with same messages occurring multiple times in the marketplace text file. On the other hand, it also emphasizes how all the components are independent, how a component can be used anywhere, and how modifications to a single component do not affect other components. ## Using marketplace texts React Intl provides multiple ways to access the marketplace text data, but the most commonly used are the `formatMessage` function and the `FormattedMessage` tag provided by React Intl. To use the `formatMessage` function, component needs to be wrapped with the `injectIntl` function which provides a render prop called `intl`. `intl` then provides all the React Intl translation functions, like `formatMessage`: ```js const SomeComponent = (props) => { const { intl } = props; const translation = intl.formatMessage({ id: 'SomeComponent.someKey', }); /* ... */ }; export default injectIntl(SomeComponent); ``` As for the `FormattedMessage` it just needs to be imported from `react-intl` and it takes the id prop: ```jsx ``` Other functions and components can be explored in the [React Intl documentation](https://github.com/yahoo/react-intl/wiki). ## Formatting React Intl uses the [FormatJS](https://formatjs.io/) formatters for shaping the messages based on given arguments. Here are a few examples on how to use FormatJS. ### Arguments Pass a named argument to the format function/component. For the following message: ```json "EnquiryForm.messageLabel": "Message to {authorDisplayName}", ``` Pass the author data in the `FormattedMessage` component: ```jsx ``` Or the the `formatMessage` function: ```js intl.formatMessage( { id: 'EnquiryForm.messageLabel' }, { authorDisplayName: 'Jane D' } ); ``` ### Pluralization With pluralization, a message can be formatted to adapt to a number argument. ```json "ManageListingsPage.youHaveListings": "You have {count} {count, plural, one {listing} other {listings}}", ``` This message takes the `count` argument and uses the `plural`, `one` and `other` keywords to format the last word of the message to be _listing_ or _listings_ based on the `count`. The pluralized message can be used with the `FormattedMessage` component: ```jsx ``` Or with the `formatMessage` function: ```js intl.formatMessage( { id: 'ManageListingsPage.youHaveListings' }, { count: 1 } ); ``` ### Select an option If you have two or more options that the message needs to show depending on another argument, you can use the `select` keyword to pass the necessary information to the message. When you use `select` in the message, you will need to specify - the variable determining which option to use (here: `mode`) - the pattern we are following (here: `select`) - the options matching each alternative you want to specify (here: `class` – there could be several options specified) - an `other` option that gets used when none of the specified alternatives matches ```json "BookingBreakdown.description": "{mode, select, day {You are booking the following days:} night {You are booking the following nights:} other {You are booking the following {unitType}:}}" ``` You can then use the message in the code with the `formatMessage` function: ```js // mode: the types of bookings or products available // on the listing page, e.g. class, package, day, night const mode = 'class'; const unitType = 'yoga class' // For { mode: 'class', unitType: 'yoga class' }, // the message will read "You are booking the following yoga class.". const description = intl.formatMessage( { id="BookingBreakdown.description" }, { mode, unitType } ); ``` You can also use the message with the `FormatMessage` component ```jsx ``` More formatting examples can be found from the [FormatJS message syntax documentation](https://formatjs.io/docs/core-concepts/icu-syntax/). --- ## How to change marketplace language Path: template/content-management/how-to-change-template-language/index.mdx # How to change marketplace language If you want the template to use a language that is not supported by default, the easiest way to do this is through Console. See our [Help Center article](https://www.sharetribe.com/help/en/articles/8418391-supported-languages-and-locales#h_4d99ff50aa) on how to do this. However, if you would still like to do this programmatically, there are some steps you need to follow. This article will walk you through those. ## Remove marketplace texts in Console In Console, navigate to Console > Build > Content > Marketplace texts. From here, you'll need to remove everything between the brackets in the editor, so that it just contains `{}`. Remember to save your progress! ## Creating a new marketplace text file 1. Copy the default `src/translations/en.json` English marketplace text file into some other file, for example `it.json` for Italian. 2. Change the messages in the new marketplace text file to the translations of the desired language. We have a few other language files available in [src/translations/](https://github.com/sharetribe/web-template/tree/master/src/translations) directory for you to start customizing marketplace texts. Even if you use [hosted marketplace texts](/template/content-management/hosted-marketplace-texts/) to manage your marketplace texts, it is still important to have a built-in language-specific marketplace text file in the template as well, so that the application can show meaningful messages for any keys missing from the Sharetribe Console marketplace text asset. ## Changing the marketplace texts used in the template Once you have the marketplace text file in place: 1. Comment out, or remove, the `messagesInLocale` definition in the [Template file src/app.js](https://github.com/sharetribe/web-template/blob/e014ddc6834a2cf1c46a514458f26656c3ce9882/src/app.js). 2. Point `messagesInLocale` to correct `.json` file, for example: ```js filename="src/app.js" ``` 3. If you are using a locale not currently supported in Console while working with moment library, you can modify [app.js](https://github.com/sharetribe/web-template/blob/main/src/app.js) to either: - Add you preferred locale to MomentLocaleLoader - Stop using the MomentLocaleLoader and instead import the locale directly in `app.js`, e.g. ```js filename="src/app.js" import 'moment/locale/fr'; const hardCodedLocale = process.env.NODE_ENV === 'test' ? 'en' : 'fr'; ``` Sharetribe supports a wide range of locales out of the box. Check Console to see if your chosen locale is already supported by navigating to Build > General > Localizations > Locale. 4. Point `messagesInLocale` to correct .json file, for example: ```js ``` It is also recommended to change _en.json_ translations. That way, accidentally deleted keys in dynamic hosted marketplace texts (in Console) won't cause the default English translations to be rendered in your custom client app. ## Changing the marketplace texts used in tests Also, in case you will translate the application and develop it forward it is wise to change the marketplace text file that the tests use. Normally tests are language agnostic as they use marketplace text keys as values. However, when adding new marketplace texts you can end up with missing marketplace text keys in tests. To change the marketplace text file used in tests change the `messages` variable in [src/util/test-helpers.js](https://github.com/sharetribe/web-template/blob/master/src/util/test-helpers.js) to match your language in use, for example: ```js ``` ## Developing the Sharetribe Web Template into a multilanguage marketplace If you intend to modify the template to handle multiple languages, it is good to note that the template is by default configured to run in single language mode, so a multilanguage marketplace requires custom development. For multiple languages, you basically have two approaches for that custom development. The first option is to create two versions of the client app, one for Language 1 and one for Language 2. They can both point to the same Marketplace API i.e. share the same listings, users, transaction processes etc. If you have a very location-specific marketplace with different locations mainly in different languages, this might be a good approach, because you can then target your UI, branding and localization more closely to the target area. Another option is to customize a single client app to provide multiple languages. For instance, you could import several language files in `src/app.js` and select which one you are going to use by modifying `src/routeConfiguration.js`, so that all the paths include a ”locale” variable. E.g. `/about` could be changed to `/:locale/about` to capture paths like `/fr/about`. In this case, it is useful to save the user's language preference to the extended data. Read more about having [a multilanguage marketplace on top of Sharetribe](/concepts/content-management/marketplace-texts/#can-i-have-a-multilanguage-marketplace). --- ## Change template texts Path: template/content-management/how-to-change-template-ui-texts/index.mdx # Change template texts The template has several types of texts that can be read in the user interface. In addition to marketplace texts, the template also has content pages, as well as some other groups of content that can be configured in the code base. ## Marketplace texts In the Sharetribe Web Template, user-facing content is not written directly into the source code. Instead, the source code uses [React Intl message formatting](https://formatjs.io/docs/intl#formatmessage) that defines keys for each meaningful piece of content, and a translator or a content creator can then define the message (i.e. the value) for each key in their language. Read more about how [Sharetribe handles marketplace texts](/concepts/content-management/marketplace-texts/). By default, the template use built-in language-specific marketplace text files to show messages in the UI. However, starting in 2022-05, operators can also modify marketplace texts in Sharetribe Console using hosted marketplace text assets. The built-in marketplace texts are merged with the hosted marketplace texts in the template, so you can use both ways of managing marketplace texts. Read more about how to [modify built-in marketplace texts in the template](/template/content-management/how-to-change-bundled-marketplace-texts/) and [how hosted marketplace texts work in the template](/template/content-management/hosted-marketplace-texts/). You may also want to change the language of the user interface entirely. Read more about [changing the language used in the template](/template/content-management/how-to-change-template-language/). In addition to marketplace texts, there are other forms of content in the client applications that operators may need to manage. ## Content pages Your marketplace also has some content pages that can be modified through Sharetribe Console. The default content pages include - About - Landing page - Privacy Policy - Terms of Service These pages are rendered by the [PageBuilder component](/template/content-management/page-builder/) in the template. In addition to these default pages, you can create your own content pages through Sharetribe Console, and [fully manage their content](/concepts/content-management/content-management-in-sharetribe/) without code changes. On the template side, you can modify [how that content is displayed](/how-to/content-management/options-prop/). ## Static pages It is possible to create fully static pages in the Sharetribe Web Template. You might want to do this if you e.g. want to create static content pages for performance reasons. More information about adding static content to the application can be found in the [How to add static pages in the template](/template/content-management/how-to-add-static-pages/) guide. ## Labels and countries There are few other cases where we haven't added marketplace texts directly to the marketplace text files. Labels for filters can be found in [_config/configListing.js_](https://github.com/sharetribe/web-template/blob/main/src/config/configListing.js), and edited in Console. [Country codes](https://github.com/sharetribe/web-template/blob/master/src/translations/countryCodes.js) are in a separate file as well. Stripe API requires country information as [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) codes. These are used when billing address is asked in _StripePaymentForm_ on _CheckoutPage_. ```shell └── src └── translations └── countryCodes.js ``` --- ## How the template renders content pages Path: template/content-management/page-builder/index.mdx # How the template renders content pages The Pages feature allows you to add, edit and manage content in Sharetribe Console. Once you have created content in Console, you can query it through the Asset Delivery API, which returns the data structured as JSON. The Sharetribe Web Template contains features that automatically render content pages from Page Asset Data. This article will walk you through the logic used to render these pages in the template.
I am using a legacy template without Pages support Read more about the code-level changes introduced in the legacy templates in the release notes of [version 10.0.0](https://github.com/sharetribe/ftw-daily/releases/tag/v10.0.0). You can find instructions on adding the Pages capability into your legacy template [in our legacy documentation](https://www.sharetribe.com/docs/template/legacy/legacy-templates/).
## What are content pages A content page is a page type that is informational by nature with text that does not frequently change. Typical content pages that you would have on your website would be an “About us” page, a “Frequently asked questions” page or a “Terms of Service” page. These pages have long sections of written text that might include images, links and videos. Sharetribe Web Template renders content pages dynamically. Content can be managed through the Pages feature, which provides the editor with a graphical interface to input text, videos, links and images. The template queries the [Asset Delivery API](https://www.sharetribe.com/api-reference/asset-delivery-api.html) to retrieve the most recent version of the content and uses it to render the content page. We refer to this data as [Page Asset Data](#page-asset-data). It reflects the content’s structure and is delivered in JSON. On page load, the template queries the Asset Delivery API to fetch the Page Asset Data needed to render the requested page. The data is subsequently stored in Redux state, which triggers a component called the PageBuilder to render the Sections, Blocks and Fields defined in the data. The [rendering pages section](#rendering-pages) explains how this happens in further detail. ## Page Asset Data Page Asset Data is a machine-readable format of the data inputted through Pages in Console. It represents the content and structure of the content page and is divided into Sections, Blocks and Fields. A single query to the Asset Delivery API will provide you with the Page Asset Data of a single content page. In other words, to render both your landing page and your FAQ page, the client will need to make two calls to the Asset Delivery API and receive two separate JSON files. Page Asset Data is always formatted in JSON. Page Asset Data nests 3 levels of information: - The Page Asset, which represents all data associated with an individual page - The Page Asset can contain an array of Sections. Sections can have a type, and there are 4 different types available by default. - Sections can contain an array of Blocks. Blocks can include text formatted in markdown. The structure outlined above is hierarchical: Blocks are always nested within Sections, and Sections are always nested within the Page Asset. Both Sections and Blocks may include Fields, which are key-value pairs encoding data such as title, ingress and background image. Read more: [Page asset schema](/references/page-asset-schema/) It is up to the client application how it renders the data received through the Asset Delivery API. Identical Page Asset Data can, for example, be rendered using entirely different visual elements on two different client applications. ## Rendering pages ### Routing and loadData calls Sharetribe Web Template uses React Router to [create routes](/template/routing/how-routing-works-in-template/) to different pages. When a user navigates to the about page, it triggers the loadData function specified in [routeConfiguration.js](https://github.com/sharetribe/web-template/blob/main/src/routing/routeConfiguration.js). Here's an example of how the privacy page route is handled: ```jsx filename="routeConfiguration.js" { path: '/privacy-policy', name: 'PrivacyPolicyPage', component: PrivacyPolicyPage, loadData: pageDataLoadingAPI.PrivacyPolicyPage.loadData, }, ``` As the content of the page is fetched using an API call, a `loadData` function is specified in [PrivacyPolicyPage.duck.js](https://github.com/sharetribe/web-template/blob/main/src/containers/PrivacyPolicyPage/PrivacyPolicyPage.duck.js): ```jsx filename="privacyPolicyPage.duck.js" export const loadData = (params, search) => (dispatch) => { const pageAsset = { privacyPolicy: `content/pages/${ASSET_NAME}.json`, }; return dispatch(fetchPageAssets(pageAsset, true)); }; ``` The function uses the fetchPageAssets function to fetch the Page Asset Data for the privacy policy page. Once the data is loaded and stored in state, the page can be fully rendered using the data stored in Page Assets. ### Predefined routes Sharetribe Web Template has four predefined routes used to generate content pages: - [PrivacyPolicy](https://github.com/sharetribe/web-template/blob/main/src/containers/PrivacyPolicyPage/PrivacyPolicyPage.js) - [TermsOfService](https://github.com/sharetribe/web-template/blob/main/src/containers/TermsOfServicePage/TermsOfServicePage.js) - [LandingPage](https://github.com/sharetribe/web-template/blob/main/src/containers/LandingPage/LandingPage.js) - [CMSPage](https://github.com/sharetribe/web-template/blob/main/src/containers/CMSPage/CMSPage.js) The first three are defined by default in Console and can not be removed. Therefore, there is a dedicated component in the template for each. For any new page created through Console, a generic component called CMSPage is used. If we compare the loadData calls in the privacy policy page and CMSPage, we can see that they differ slightly. The PrivacyPolicyPage.duck.js file uses a predefined path for fetching the page asset `content/pages/privacy-policy` ```js filename="privacyPolicyPage.duck.js" export const ASSET_NAME = 'privacy-policy'; export const loadData = (params, search) => (dispatch) => { const pageAsset = { privacyPolicy: `content/pages/${ASSET_NAME}.json`, }; return dispatch(fetchPageAssets(pageAsset, true)); }; ``` Whereas the CMSPage uses a dynamic ID that is passed through the URL ```js filename="CMSPage.duck.js" export const loadData = (params, search) => (dispatch) => { const pageId = params.pageId; const pageAsset = { [pageId]: `content/pages/${pageId}.json` }; const hasFallbackContent = false; return dispatch(fetchPageAssets(pageAsset, hasFallbackContent)); }; ``` The template can use hardcoded asset names for Pages included by default in Console, as the paths are static and not subject to change. The Pages included by default are the Landing Page, Terms of Service page and Privacy Policy page. The CMSPage component, on the other hand, is used to render any new Pages created by the user, which are assigned an ID on creation. ### PageBuilder Sharetribe Web Template uses a React component called the PageBuilder to dynamically render content pages using Page Asset Data. You can find the PageBuilder in the containers directory: The PageBuilder component receives the Page Asset Data as a prop, and uses it to render the content page. If no Page Asset Data is available, it renders a fallback page. ```jsx filename="pageBuilder.js" const PageBuilder = props => { const { pageAssetsData, inProgress, fallbackPage, options, ...pageProps } = props; if (!pageAssetsData && fallbackPage && !inProgress) { return fallbackPage; } ``` The PageBuilder will invoke the SectionBuilder component if Sections are present in the Page Asset Data. ```jsx filename="pageBuilder.js" const data = pageAssetsData || {}; const sectionsData = data?.sections || []; ; ``` Subsequently, the SectionBuilder will pass data on to the BlockBuilder if an array of Blocks is present. To render e.g. headers, links and images, the template defines a Field component that is used in both the BlockBuilder and SectionBuilder. Fields are the most granular form of data in Page Asset Data. The Field component validates and sanitises any data before it is rendered. ## Section and Block types Using the Pages feature in Console, you can define a section type. The template recognises all Section types and renders each using a different presentational component. There are four Section types: - Articles, meant for copy text and using a narrow one column layout optimized for reading - Carousel, an image carousel consisting from images uploaded through Console - Columns, content blocks rendered in a 1, 2, 3 or 4 column grid - Features, text and media displayed side by side in alternating order The corresponding Section component is selected using the getComponent function in the SectionBuilder: ```jsx filename="SectionBuilder.js" const Section = getComponent(section.sectionType); ``` The getComponent function uses the defaultSectionComponents object to select the correct React component: ```jsx filename="SectionBuilder.js" const defaultSectionComponents = { article: { component: SectionArticle }, carousel: { component: SectionCarousel }, columns: { component: SectionColumns }, features: { component: SectionFeatures }, }; ``` Each section component is wrapped in a SectionContainer. You can use it to apply styling that should be present in each component. Default components can be overridden or edited. Remember that the changes will be global and reflected on each content page. If you want to change a Section component on a specific page, you can use the [options prop to override a page-level component](/how-to/content-management/options-prop/). Blocks also have a type property. Currently, Page Asset Data only supports a single Block type. ## Fallback pages As the content of the page is retrieved over a network connection, it is important to prepare for a scenario where data is unavailable due to e.g. a network issue. The template uses fallback data if loading the Page Asset Data through the Asset Delivery API fails. Fallback pages are specified for page-level components and are included out of the box for the Landing page, Terms of Service page and Privacy Policy page. A fallback page is constructed similarly to how a dynamic content page is. It uses the PageBuilder component, but instead of dynamically retrieving Page Asset Data, it is given the pageAssetsData prop as a predefined JSON asset. That data can be defined inline or in a separate file. The fallback data should adhere to the structure and format used in Page Asset Data. A fallback page is defined in the same directory that the page level component is defined in. For example, you will find the fallback page of the Privacy Policy page under `containers/PrivacyPolicyPage/FallbackPage.js`: ## Maintenance mode If the marketplace is missing some mandatory configurations, you will see a fallback page with the title "Maintenance mode". ![Maintenance mode page](./maintenance-mode.png) To fix the situation, make sure that you have added the following mandatory configurations in your Console: - [Branding](https://console.sharetribe.com/a/design/branding) - [Listing type](https://console.sharetribe.com/a/listings/listing-types) - [Listing fields](https://console.sharetribe.com/a/listings/listing-fields) - [Minimum transaction size](https://console.sharetribe.com/a/transactions/minimum-transaction-size) After you have made sure you have all these configurations added in your Console, refresh the browser. Your marketplace should now show up with the configurations you added. If Maintenance mode still persists, check your browser developer tools for further errors. You can also always reach out to our support through the chat widget in your Console, and we will be happy to troubleshoot the issue with you! #### Additional tips on troubleshooting the maintenance mode error message If your marketplace runs without issue on your local, but you see the "maintenance mode" message on a cloud deployment (e.g., in Heroku or Render), you should check that you have the correct Client ID set in your environment variables. The marketplace uses the Client ID to fetch your marketplace assets through our API. You might also see the "maintenance mode" message if a [Listing Field ID](https://www.sharetribe.com/help/en/articles/8413285-how-to-add-and-edit-listing-fields) you've defined via Console clashes with any of the [built-in public data fields](https://github.com/sharetribe/web-template/blob/main/src/util/configHelpers.js#L25-L33). If this is the case, you will see a message about this when you open the console view in your browser's developer tools tab. --- ## Deploy to Heroku Path: template/hosting/how-to-deploy-template-to-heroku/index.mdx # Deploy to Heroku This guide provides a practical demonstration of how to deploy the Sharetribe Web Template to Heroku. Heroku is one of the most popular cloud hosting service providers, and because of that, there is a lot of information online that can help you solve and debug potential problems with your deployment. Unlike [Render](https://www.render.com), Heroku does not provide a free tier. Before deploying your marketplace to Heroku, you need to create three accounts: - [Heroku](https://www.heroku.com/) - [Stripe](https://stripe.com/) - [Mapbox](https://www.mapbox.com/) ## Deploying the template to Heroku {

Create a Heroku account

} Go to [Heroku](https://www.heroku.com/pricing) and create a new account if you do not have one. {

Create a new app

} Create a new app in the Heroku dashboard. ![Create new app button in Heroku](./heroku-create-new-app.png) {

Change the environment variables

} In Heroku, you can configure environment variables from the [Heroku dashboard](https://devcenter.heroku.com/articles/config-vars#using-the-heroku-dashboard). Go to the Settings page of your new app and reveal Config Vars: ![Heroku: reveal Config Vars](./heroku-config-vars.png) Then add the following environment variables as Config Vars: - `REACT_APP_SHARETRIBE_SDK_CLIENT_ID` Sharetribe client ID. Check this from [Console](https://console.sharetribe.com/advanced/applications). - `SHARETRIBE_SDK_CLIENT_SECRET` Sharetribe client secret. Check this from [Console](https://console.sharetribe.com/advanced/applications). - `REACT_APP_STRIPE_PUBLISHABLE_KEY` Stripe publishable API key for generating tokens with Stripe API. Use the test key (prefix `pk_test`) for development. - `REACT_APP_MAPBOX_ACCESS_TOKEN` If you are using Mapbox instead of Google Maps - `REACT_APP_MARKETPLACE_ROOT_URL` Canonical root URL of the marketplace. Remove trailing slash from the domain.
E.g. _`https://.herokuapp.com`_ - `REACT_APP_MARKETPLACE_NAME` Marketplace name in self-hosted marketplaces is set through environment variables. If not set, this defaults to 'Biketribe', or whatever value you have set in src/config/configDefault.js. - `NODE_ENV` Defines whether the application is run in production or development mode. Use 'development' for development and 'production' for production.
Use value: 'production' - `REACT_APP_ENV` A more fine-grained env definition than `NODE_ENV`. For example, this sends environment info to the logging service Sentry. (If you have enabled it with `REACT_APP_SENTRY_DSN`).
For this setup, use value: 'development' - `REACT_APP_SHARETRIBE_USING_SSL` Redirect HTTP to HTTPS?
Use value: true - `SERVER_SHARETRIBE_TRUST_PROXY` Set this when running the app behind a reverse proxy, e.g. in Heroku.
Use value: true - `REACT_APP_CSP` Content Security Policy (CSP). Read more from [this article](/template/security/how-to-set-up-csp-for-template/).
Accepts values: _block_ and _report_. The recommended value is _block_. If you change these values later on, _you need to deploy the app again_. Environment variables are baked into the static build files of the web app - so a new build is required. {

Add a Node.js buildpack

} Go to the Settings page of your new app and add the official buildpack: _heroku/nodejs_ ![Add buildpack](./heroku-add-buildpack.png) {

Connect the Heroku app to Github

} Go to the Deploy page of your new app and [connect the app with Github](https://devcenter.heroku.com/articles/github-integration#enabling-github-integration). ![Heroku: Connect the app with Github repository](./heroku-connect-to-github.png) After that, you can deploy the app manually or enable automatic deploy from your default branch (usually named as _main_ or _master_). If everything works, your app should be available in a URL that looks a bit like this: `HTTPS://.herokuapp.com` ## Set up domains and certificates Heroku manages SSL certificates automatically for new applications. You can change your domain and SSH settings in the _Settings tab_. Read more from Heroku docs: - [Custom Domain Names for Apps](https://devcenter.heroku.com/articles/custom-domains) - [Automated Certificate Management](https://devcenter.heroku.com/articles/automated-certificate-management) ![Heroku settings](./heroku-domains.png) ## Heroku logs You can find your application's logs by clicking button _"More"_ in the upper right corner and selecting _"View logs"_ from the opening dropdown. Logs can be useful if there are problems when deploying the app. ![Heroku logs](./heroku-logs.png) ## Troubleshooting Heroku By default, Heroku will use latest Long-Term-Support (LTS) version of Node.js. So, you might want to specify that your dev and production environments use the same Node version as your local machine when you run `yarn run dev-server`. This can be done by adding an `engines` section to the `package.json`. Read more from Heroku's [Node.js Support guide](https://devcenter.heroku.com/articles/nodejs-support#specifying-a-node-js-version). You should also check that the _environment variables_ in your local environment matches with _Config Vars_ in Heroku app settings. --- ## Deploy to production Path: template/hosting/how-to-deploy-template-to-production/index.mdx # Deploy to production ## What is a production environment? Note that you need to enable [Hosting mode](https://www.sharetribe.com/help/en/articles/8944305) through the Sharetribe Console before you can connect your custom application to your Live environment. A production environment hosts and executes the code that runs your live marketplace and serves it to the public internet. The version of your marketplace intended for real-life use with real money runs in the production environment. Typically, alongside your production environment, you will also host another version of your client environment: a test environment. Having several environments is a common practice in software development. The test environment is most often a clone of the production environment intended for testing new features before being deployed to production. Once a development team deems a feature production-ready, i.e. fit for a live audience, they can deploy it to production. A workflow like this helps prevent bugs and unfinished code from being released to your users. Sharetribe offers [three different environment types](https://www.sharetribe.com/docs/concepts/development/sharetribe-environments/#environment-types) – Live, Test, and Dev. You should connect your client application with the corresponding marketplace environment, i.e. your client environment intended for development should use environment variables that point to your dev environment in Sharetribe. More specifically, the workflow recommended with Sharetribe is that you have three deployments of your client application: - production deployment, connected to your Live environment and running real transactions - test deployment, connected to your Test environment and intended for previewing no-code changes - dev deployment, connected to your Dev environment and intended for testing and previewing code-level changes. We recommend that you keep your production and test deployments identical, so that operators can preview their no-code changes reliably. Read more: [Sharetribe environments](/concepts/development/sharetribe-environments/#workflow-between-the-three-environments). ## Where to host your application? There are many hosting providers to choose from when considering where to host your marketplace. Our official recommendation is to host your marketplace on Heroku or Render for a hassle-free installation. However, you are free to host your marketplace elsewhere. The Sharetribe Web Template should be compatible with any hosting provider as long as they allow you to run a Node.js/Express server. Many essential functions in the template rely on a small Node.js/Express server (such as server-side rendering, SSO and transitioning privileged transactions). Serverless service providers such as Netlify and Vercel are unsuitable for hosting the template as they don't allow you to host a server. When choosing a hosting provider, you should not only consider the cheapest option. Scalability, tools, service-level agreements and available computing resources are examples of factors that should weigh in when choosing a hosting provider. It is also possible that hosting providers' prices and services may change over time, e.g. [Heroku discontinued](https://techcrunch.com/2022/08/25/heroku-announces-plans-to-eliminate-free-plans-blaming-fraud-and-abuse/) its popular free tier in October 2022. As of the time of writing, [Render](https://www.render.com) and [Fly.io](https://fly.io) continue to provide a free tier, which you can use, e.g. to host a test application. You will have to move to a paid plan for a production-level deployment to ensure consistent uptime and computing resources for your marketplace. Other alternatives you can look into include [AWS](https://aws.amazon.com/), [Google Cloud](https://cloud.google.com/), [Digital Ocean](https://www.digitalocean.com/) and [Microsoft Azure](https://azure.microsoft.com/). If your deployment solution requires using containers, you can refer to our guide on [running the template in a Docker container](/template/hosting/run-template-with-docker/). ## Deploying to production If you are looking to deploy your marketplace on either Heroku or Render, please read our detailed deployment guides for both [Heroku](/template/hosting/how-to-deploy-template-to-heroku/) and [Render](/tutorial/deploy-to-render/#deploy-to-render). Deploying your marketplace to production is a four-step process: 1. [Install dependencies](#install-dependencies) 2. [Set environment variables](#environment-variables) 3. [Build the app](#building-the-app) 4. [Run the node server](#starting-the-app) ### Install dependencies In your project root, install dependency libraries: ```shell yarn install ``` In some hosting environments, you might already have environment variables set before _yarn install_ is called. If the NODE_ENV variable is set to "production", Yarn won't [install dev dependencies](https://classic.yarnpkg.com/lang/en/docs/cli/install/#toc-yarn-install-production-true-false). This can cause issues because Sharetribe Web Template lists Webpack-related packages as dev dependencies since they are not used in the production runtime. To avoid errors from the build script, you can run the following command instead: ```shell yarn install --production=false ``` For example, on Render.com, you can set the build command to this: ![Build command on Render.com](./render.com-build-command.png) ### Environment variables If you are transitioning from a Sharetribe hosted marketplace to a self-hosted marketplace and you are using Social Logins (e.g. Google or Facebook login), you will need to enable the logins through [environment variables](/template/configuration/template-env/). The configuration in Console only works for Sharetribe hosted marketplaces. See [here how to enable Google Login](/how-to/users-and-authentication/enable-google-login/) and [Facebook login](/how-to/users-and-authentication/enable-facebook-login/) for self-hosted marketplaces. For a full list of possible environment variables, see the [Environment configuration variables](/template/configuration/template-env/) reference for more information. To deploy your marketplace, you need to add at least the following variables: - **`NODE_ENV`** Use the value 'production'. - **`PORT`** You must set a port if the production environment does not set one by default. Heroku and Render define a port automatically. - **`REACT_APP_SHARETRIBE_SDK_CLIENT_ID`** Your client ID. You can find it in [Sharetribe Console](https://console.sharetribe.com/advanced/applications). - **`SHARETRIBE_SDK_CLIENT_SECRET`** Your client secret. You can find it in [Sharetribe Console](https://console.sharetribe.com/advanced/applications). - **`REACT_APP_STRIPE_PUBLISHABLE_KEY`** Stripe publishable API key for generating tokens with Stripe API. You find it on the Stripe [API keys](https://dashboard.stripe.com/account/apikeys) page. You will also need to add the secret key in Sharetribe Console. - **`REACT_APP_MARKETPLACE_ROOT_URL`** This is the root URL of the marketplace. For example: `https://the-name-of-your-app.herokuapp.com`. The template uses the root URL for social media sharing and SEO optimization. - **`REACT_APP_MARKETPLACE_NAME`** The Marketplace name in self-hosted marketplaces is set through environment variables. If not set, this will default to 'Biketribe', or whatever hard-coded value you have set in [src/config/configDefault.js](https://github.com/sharetribe/web-template/blob/main/src/config/configDefault.js#L36). - **`REACT_APP_MAPBOX_ACCESS_TOKEN`** The Mapbox access token is specified via Console, under **Integrations** > **Map**. The template fetches this access token via [assets](/references/assets/). This environment variable is used as a fallback, and therefore is not mandatory. See the [How to set up Mapbox or Google Maps for location services](https://www.sharetribe.com/help/en/articles/8676185-how-to-set-up-mapbox-or-google-maps-for-location-services) guide for more information. ### Building the app Running the following command builds the app for production to the build folder. It correctly bundles React in production mode and optimizes the build for the best performance. ```bash yarn build ``` After this, your app is ready to be deployed. ### Start the server To start the server, you need to run: ```bash yarn start ``` --- ## How to run Sharetribe Web Template in a Docker container Path: template/hosting/run-template-with-docker/index.mdx # How to run Sharetribe Web Template in a Docker container Depending on your deployment infrastructure, you may want to run your Sharetribe Web Template based marketplace application in a Docker container. This article features a sample Dockerfile you can use to create your Docker image. To learn more about Docker and its key concepts, check out [Docker documentation](https://docs.docker.com/) or watch a [Docker introduction video](https://youtu.be/pTFZFxd4hOI). This guide assumes you have Docker installed and running on your development machine. To run your template app with Docker, you will need to - add your Dockerfile - build the Docker image, and - run the Docker container. ## Add your Dockerfile For creating a Docker image of your template application, you will need to add a Dockerfile. Add a file titled _Dockerfile_ with no file extension to the root of your template folder, on the same level as your _package.json_ file. Copy the following contents, paste them to the newly created _Dockerfile_ and save. ``` FROM node:16 WORKDIR /home/node/app COPY package.json ./ COPY yarn.lock ./ RUN yarn install COPY . . ENV PORT=4000 ENV NODE_ENV=production EXPOSE 4000 RUN yarn run build USER node CMD ["yarn", "start"] ``` In addition, you need to add a _.dockerignore_ file in the same root folder. Copy the following contents, paste them to the newly created ._dockerignore_ file, and save. ``` # Ignore node_modules because they get installed in the Dockerfile. node_modules ``` ## Build your Docker image and run the Docker container To build your Docker image, open your command line and navigate to the root of your template folder. Run the following command – be careful to include the final `.` , as it indicates that the Dockerfile is in the current directory. ```shell $ docker build -t sharetribe-docker . ``` The build step can take a while. After the build step completes, you can start a container using the image you created. ```shell $ docker run -dp 4000:4000 sharetribe-docker ``` You can now visit _http://localhost:4000_ on your local machine to see that the container is running your application. --- ## Template Path: template/index.mdx # Sharetribe Web Template Articles that help you to understand how the Sharetribe Web Template works. ## Introduction ## Configuration ## Content Management ## Availability Management ## Styling ## Routing ## State Management ## Maps ## Payments ## Performance ## Security ## Testing ## Hosting ## Analytics ## Legacy --- ## Customization checklist Path: template/introduction/customization-checklist/index.mdx # Customization checklist The Sharetribe Web Template provides a great starting point for developing your marketplace. This article provides a reference list of articles and guides you can use when you start customising your marketplace's visual appearance, content and configurations. ## Visual appearance You can edit the default styles of the template via the branding tab in Console. For more advanced changes you can: - [Learn to style the template using CSS](/tutorial/first-edit/) - Learn more about the [CSS architecture in the Sharetribe Web Template](/template/styling/how-to-customize-template-styles/) ## Configuration The configuration file of your marketplace template is a valuable resource that allows you to easily adjust a wide range of options in your marketplace. Configuration files can be found in the [src/config directory](https://github.com/sharetribe/web-template/tree/main/src/config). Before you begin modifying the configuration files, it is important to ensure that you have properly configured any environment variables that your marketplace uses. These variables are typically used to store sensitive information such as API keys, and must be set up correctly in order for your marketplace to function properly. - Configure your [environment variables](/template/configuration/template-env/). You can also run `yarn run config` in the root directory of the template, which will walk you through the setup process. ## Other optional changes In addition to customizing the default styles and configuration options of your custom developed marketplace, there are a number of other changes you may want to make in code. Some of these options include: - [Customize pricing](/tutorial/customize-pricing/) - [Update your transaction process](/tutorial/create-transaction-process/) - [Create new static pages](/template/content-management/how-to-add-static-pages/) - [Update the routing in your marketplace](/template/routing/how-routing-works-in-template/) - Update transaction email templates. For more information, see [Edit email templates with Sharetribe CLI](/how-to/emails-and-notifications/edit-email-templates-with-sharetribe-cli/) tutorial and [Email templates](/references/email-templates/) reference article. --- ## Customizing the template Path: template/introduction/how-to-customize-template/index.mdx # Customizing the template There are some best practices we recommend that you follow when you are developing on top of the Sharetribe Web Template. This article outlines those best practices and links to articles with more details if you want to dive deeper. ## Prerequisites Make sure you have followed these steps to get your development environment up and running. ### Create your client ID and secret The Sharetribe Web Template is a React application built on top of the [Marketplace API](/concepts/api-sdk/marketplace-api-integration-api/). While you can create a marketplace client application from scratch using just the API, it requires a lot of effort and we recommend that you use a template as a starting point for customizations. To use the Marketplace API, you will need a client ID. You can obtain one in your [Sharetribe Console > Advanced > Applications](https://console.sharetribe.com/advanced/applications). ### Getting started with the template If you are new to Sharetribe or the Sharetribe Web Template, we recommend reading these articles before starting to work on development: - [Introducing Sharetribe](/introduction/) - [What development skills are needed?](/introduction/development-skills/) - [Getting started](/introduction/getting-started-with-web-template/) The [tutorial introduction](/tutorial/#prerequisites) will also walk you through creating a GitHub repository. When you start a new project, you should create a new Git repository for your project and add the template repository as a remote repository. That allows you to update your code with the latest changes from the upstream repository, as described later in this article. ## Pull in the latest upstream changes To update your project with the newest changes from the remote repository, you need to pull these changes from the upstream remote. Pulling the newest changes from the upstream remote might be hard or impossible, depending on the extent of the changes you have made to the template. The template is a starting point for development rather than something that you should regularly update. You should follow the [tutorial](/tutorial/) to set up a local development environment and connect it to GitHub. Run the following commands in a new branch. 1. Create a new branch and switch into that branch: ```shell git checkout -b updates-from-upstream ``` 2. Fetch the latest changes from the upstream repository: ```shell git fetch upstream ``` 3. Merge the changes to your local branch ```shell git merge upstream/main ``` 4. Fix possible merge conflicts, commit, and push/deploy. If you have forked the repository instead of setting a remote, see how to [sync a fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/syncing-a-fork). ## Installing dependencies In your project root, install dependency libraries: ```shell yarn install ``` ## Configuring environment variables You need to configure some environment variables before deploying or running the template locally. You can simply run the following command in the root of your project directory: ```shell yarn run config ``` This command will create `.env` file and guide you trough setting up the required environment variables. ```shell └── .env ``` The `.env` file is the place to add your _local_ configuration. It is ignored by Git, so you will have to add the corresponding configuration also to your server environment. When deploying the template to Render or Heroku, you need to configure the environment variables in the hosting platform. See our article on deploying the template to [Render](/tutorial/deploy-to-render/#deploy-to-render) for more information. See the full list of [environment variables](/template/configuration/template-env/) for more information. For in-app configurations, see the [src/config directory](https://github.com/sharetribe/web-template/tree/main/src/config). ```shell └── src └── config ``` ## Development To develop the application and to see changes live, start the frontend development server: ```shell yarn run dev ```
Extra: troubleshooting **Known issues:** - Adding/changing `import`s may not be synced properly with ESLint. You may see an error `Unable to resolve path to module` even though the module exists in the right path. Restarting the server doesn't help. To solve the issue, you need to make a change to the file where the error occurs.
#### Development with the actual Node.js server The usual way to develop the application is to use the frontend development server (see above). However, in production you likely want to use the server-side rendering (SSR) setup. To develop against the actual server locally, run: ```shell yarn run dev-server ``` This runs the frontend production build and starts the Express.js server in [server/index.js](https://github.com/sharetribe/web-template/blob/main/server/index.js). ```shell └── server └── index.js ``` The server is automatically restarted when changes are detected in the [server](https://github.com/sharetribe/web-template/blob/main/server/) directory. This server does **not** detect changes in the frontend application code. For that you need to rebuild the client bundle by restarting the server manually. ## Tests To start the test watcher, run ```shell yarn test ``` For more information on tests, see the documentation on [how to test the template](/template/testing/how-to-test-template/). ## Further reading There are many things that you should change in the default template and many more that you can change. For more information, check the [template customization checklist](/template/introduction/customization-checklist/) documentation before publishing your site. Also see the [tutorial](/tutorial/) and other articles in the [Sharetribe Web Template](/template/introduction/customization-checklist/) and How-to categories. --- ## Sharetribe Web Template Path: template/introduction/sharetribe-web-template/index.mdx # Sharetribe Web Template ## Introduction [The Sharetribe Web Template](https://github.com/sharetribe/web-template) is a React template meant to function as a starting point for the development of your marketplace. It supports the features available through the Sharetribe APIs, and is easily customized. We recommend you be familiar with React, Redux and CSS Modules before you start working on the template. Read more about [what development skills are needed](/introduction/development-skills/) and [how to install and run the template locally](/introduction/getting-started-with-web-template/). Access the [GitHub repository for the template here](https://github.com/sharetribe/web-template). ## Technical overview The Sharetribe Web Template is a React application built on top of a vendored version of the now deprecated [Create React App (CRA)](https://create-react-app.dev/). In addition to the basic CRA setup, the template uses server-side rendering (SSR) and code splitting. The template also includes a Node.js server, which enables SSR and other essential features which require communicating with the Sharetribe APIs without allowing the client to modify the requests. The included Node.js server enables server-side rendering, which helps to improve search engine optimization and speed up the application's rendering. You should consider this requirement when choosing a hosting provider for your marketplace project. Serverless hosting platforms are not compatible with the template, as they are not equipped to run a server. You need to use a hosting provider that supports running a Node.js/Express.js server to run the Sharetribe Web Template without any issues. For a complete overview of the technologies used in the template, read more in the [development skills article](/introduction/development-skills/). ## Working with the template - Familiarize yourself with the tech stack: make sure you have a good understanding of the technologies and libraries that the template is built on, including React, Redux, and CSS Modules. - Read the documentation: the template is well-documented, and reading the documentation can be a helpful way to learn more about how the template works and how to customize it. A good place to start is the [tutorial](/tutorial/). - Start with the [configuration variables](/template/configuration/variables/): the template includes a number of configuration variables that can be used to make quick and easy changes to the layout, branding, and functionality of the template. Experiment with these variables to see what kind of changes you can make. - Customize the code as needed: the template is designed to be customizable, and you can make any desired changes to the code. There are no limits to how far you can customize the template. See our catalog of how-to articles or the [customization checklist](/template/introduction/customization-checklist/) for advice and inspiration. --- ## Legacy templates Path: template/legacy/legacy-templates/index.mdx # Legacy templates {
View the legacy documentation here } ## Deprecated templates The Sharetribe Web Template replaces three separate templates that were previously used for daily bookings ([ftw-daily](https://github.com/sharetribe/ftw-daily)), product purchases ([ftw-product](https://github.com/sharetribe/ftw-product)), and hourly bookings ([ftw-hourly](https://github.com/sharetribe/ftw-hourly)). The new template combines the functionality of these three templates, allowing you to configure it for either product bookings or hourly and daily bookings by making simple changes to the configuration files. If you are just beginning to develop a marketplace, we recommend that you start with the Sharetribe Web Template. If you have already built your marketplace on one of the legacy templates, you can still continue use it with Sharetribe APIs and our support will continue to help you with development related issues. Read more about the new Sharetribe Web Template in the [introduction article](/template/introduction/sharetribe-web-template/) and refer to the legacy documentation here. ## Migrate to the Sharetribe Web Template If you have already built a fully functional marketplace on top of one of the deprecated templates, there is likely no need to migrate to the new template. All templates, including the deprecated ftw-daily, ftw-hourly and ftw-product and the new Sharetribe Web Template, are meant to act as starting points for development. We will never introduce breaking changes to our APIs, and all deprecated templates will remain functional. Therefore, you should only consider migrating to the new template if you have just started developing your marketplace or if your existing marketplace has little to no custom features. To view the changes introduced in the new template, you can review the pull request for the update. This will give you a detailed look at the specific changes and updates that have been made. This article has more context on what changes are required in your marketplace environment if you want to migrate from a legacy template to Sharetribe Web Template: - [Moving your marketplace from a legacy template to Sharetribe Web Template](/template/legacy/legacy-to-new-template/) --- ## From legacy template to Sharetribe Web Template Path: template/legacy/legacy-to-new-template/index.mdx # Moving your marketplace from a legacy template to Sharetribe Web Template The Sharetribe product is built on top of the same technology that powered our legacy product Flex. This means that if you want to update your existing marketplace to use the new Sharetribe Web Template, you can still keep using the same Console, and your marketplace data will still be available. However, you will need to make some changes to your data and environment configurations before you move to the Sharetribe Web Template. This article describes things you need to consider when planning this change. ## Check that you have the correct transaction processes in your environment If you have created your marketplace environment prior to the 25th of April 2023, it is good to note that there are four new transaction processes the template uses, and those processes may not be in your Sharetribe marketplace by default. You can find the transaction processes in [/ext/transaction-processes/](https://github.com/sharetribe/web-template/tree/main/ext/transaction-processes) in the repository. To use the template, you will need to have the transaction processes in your Sharetribe environment. [Follow these steps](/how-to/transaction-process/create-new-transaction-process-with-cli/) to create both processes in your environment through Sharetribe CLI. ## Update your listing data You can move from a legacy template based client app to a Sharetribe Web Template based client and keep managing your existing listings in the new application. To do so, you need to do the following things – first in your Test or Dev environment, and finally in your Live environment. Do not make changes to your Live data before testing those changes thoroughly in either Test or Dev environments! ### Create your listing types and test listings in Console In Console, create the listing types you want to have available on your marketplace. Create a listing for each of the listing types with the Sharetribe Web Template. You can connect a Sharetribe Web Template with your existing environment by following these instructions: - [Getting started with the Sharetribe Web Template](/introduction/getting-started-with-web-template/) Then, review those new listings in Console and make a note of the values for the three listed listing type related attributes: - listingType - transactionProcessAlias - unitType ### Update your existing listings with the correct attributes For each of your existing listings, edit the listing to add the three attributes with the correct values according to the listing type you want to assign to the listing. You can create an Integration API script to update the attributes to your listings - if you only want to have a single listing type for all listings on your marketplace - or if you have a way of programmatically determining which listing belongs to which listing type – for example if there is an attribute on your existing listings that always indicates the correct listing type That way, you don't need to update all listings manually in Console. We have an example script that you can use as the basis for this development work. - [Integration API example script: bulk update listings](https://github.com/sharetribe/integration-api-examples/blob/master/scripts/bulk-update-listings.js) ### Update other attributes on the marketplace Once you've added the three necessary attributes to your listings, your providers can then update other possible attributes (such as categories, availability, price variations etc.) in the listing editing flow. ## Is it possible to combine a legacy custom code marketplace with the Sharetribe Web Template? The Sharetribe Web Template has a lot of development work on top of the legacy template versions. This means that even if it was technically possible to take an upstream update to your legacy codebase, combining your custom codebase and the Sharetribe Web Template codebase with a merge would be slow and expensive. If you do have custom developed features the Sharetribe Web Template does not have that you want to keep in your marketplace, you will need to implement those features on top of the Sharetribe Web Template codebase. Depending on the feature, the effort required might range from moderate to extensive. On the server side, the differences between the legacy templates and Sharetribe Web Template are not as extensive. This means that if your feature code is mostly on the server side, you may be able to reuse portions of your current code. Client-side changes will most likely require a full rewrite. --- ## Map configurations Path: template/maps/configure-maps/index.mdx # Map configurations ## Configure default locations You can configure the search field on the landing page to show the user a predefined list of locations from which to search. Enabling this feature can make searching for common locations faster for your users and reduce the need to call the Mapbox Geolocation API. ![A screenshot of the search bar in Sharetribe Web Template](./location.png) This feature is not enabled by default. To enable it, you need to add the locations to the [configDefaultLocationSearches.js](https://github.com/sharetribe/web-template/blob/main/src/config/configDefaultLocationSearches.js#L14) file. To disable the first result on the list to search for the user's "Current location", you can change the configuration variable [`suggestCurrentLocation` in the configMaps.js file](https://github.com/sharetribe/web-template/blob/main/src/config/configMaps.js#L23) to false. The [`currentLocationBoundsDistance`](https://github.com/sharetribe/web-template/blob/main/src/config/configMaps.js#L27) variable (found in the same file) defines the distance in meters for calculating the bounding box around the current location. For a more detailed guide on how to change the default locations, refer [to the tutorial](/tutorial/change-default-locations/). ## Configure fallback bounding boxes The Mapbox Geocoding API does not always return a bounding box with the results. The [SearchMap](https://github.com/sharetribe/web-template/blob/main/src/containers/SearchPage/SearchMap/SearchMap.js) component needs a bounding box to adjust the zoom level of the map when displaying a location. Suppose the Geocoding API does not return a bounding box. In that case, the map uses the default values defined in [GeocoderMapbox.js](https://github.com/sharetribe/web-template/blob/main/src/components/LocationAutocompleteInput/GeocoderMapbox.js). The configuration specifies a default distance to generate the bounding box with for all different Mapbox Geocoding [data types](https://docs.mapbox.com/api/search/geocoding/#data-types). ## Restrict location autocomplete to specific country or countries If your marketplace works only in a specific country or countries it might be a good idea to limit the location autocomplete to those countries. You can specify whether to use the limitation in [config/configMaps.js](https://github.com/sharetribe/web-template/blob/main/src/config/configMaps.js#L48). Search for variable `countryLimit` and uncomment the line to make it active. Provide the country or countries in an array using [ISO 3166 alpha 2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) format (eg. GB, US, FI). If there are multiple values, separate them with commas. ## How to use other map providers The default map setup of the template uses library called [mapbox-gl-js](https://docs.mapbox.com/mapbox-gl-js/api/). It supports quite many other map providers too. Thus, if you wish to use a map provider other than Google Maps or Mapbox, first check if the map provider you are considering is supporting this library. If they are, the change might be quite easy. Note: if you change the map tile provider you should also change geocoding API too (i.e. the API endpoint for `LocationAutocompleteInput` component). --- ## How saving a payment card works in the Sharetribe Web Template Path: template/payments/save-payment-card/index.mdx # How saving a payment card works in the Sharetribe Web Template When a customer first comes to the marketplace and initiates a payment for a transaction, there are several form fields the user needs to fill: expiration month, card verification code (CVC), card holder's name and possibly other billing details. To improve the user experience for returning customers, it is good to have an option to save payment card details for future bookings. If there is an existing payment card available, the user can just click the "Send request" button to complete checkout page. From that point forward, the typical case is that customer pays upfront (i.e. makes an [on-session](https://stripe.com/docs/payments/cards/reusing-cards#charging-on-session) payment). In the default-booking process, a preauthorization is created: the money is reserved, but not yet moved to Stripe. When a provider accepts the request, the preauthorization is captured (i.e. the payment is charged from the card and money moved to Stripe and held on the connected account of the provider until payout). In the default-purchase and default-negotiation processes, the payment is created and captured immediately. The actual payout happens when the transaction completes. A payment card also needs to be saved for off-session payments if those are in use. They are automatic one-time payments that happen when the user is not interacting with your application. You can read more about off-session payments from a separate [article](/concepts/payments/off-session-payments-in-transaction-process/). ## Saving card details There are 2 ways to save payment card details: - saving cards details without making an initial payment - saving cards details after a payment The former of those can be done in payment methods page (profile menu -> account settings -> payment methods), but you can also save payment card details on the checkout page when making a one-time payment. Currently, it is only possible to save one card, which becomes the default payment method. I.e. if there is already one payment card saved, your only option is to replace the card with a new one. ### Saving cards without making an initial payment If the user has no existing default payment card, `PaymentMethodsPage` component will show just a form to save payment card details: ![Payment methods page](./payment-methods-page.png 'Payment methods page without default payment method.') If there is a default payment methods saved, user will see a dropdown instead, where the current default method is selected. There are two options the user can do: replace the current payment card (it's an option inside the dropdown) or delete the current payment card. ![Payment methods page saved card](./payment-methods-page-saved-card.png 'Payment methods page with default payment method.') Under the hood, saving a new payment method needs to complete following steps: - Create Stripe [Setup Intent](https://www.sharetribe.com/api-reference/marketplace.html#stripe-setup-intents) through Sharetribe API to obtain the client secret. - Call `stripe.handleCardSetup` with the client secret - `stripe.handleCardSetup` will handle user actions like 3D Secure authentication - Save payment method: - Create a new Stripe Customer if needed and attach the new payment method to `stripeCustomer` entity. - There's no replace or update option for `defaultPaymentMethod` entity. To replace a payment card, one needs to remove previous card first and then add a new payment method. ### Saving cards after a payment If the user doesn't have a default payment card saved, or if they are making a one-time payment with a different payment card, there's an option (checkbox) to save the new payment card as the default payment method. ![One-time payment and saving card details for future use](./one-time-payment.png 'One-time payment and saving card details for future use') If the default payment method exists, user needs to select one-time payment first and then they are able to replace the default card. ![Pay with one-time payment card](./pay-with-new-one-time-card.png 'Pay with one-time payment card') **Under the hood, there are few changes made to the Payment Intents checkout flow:** - We need to know if the user has already saved a default payment method: - Fetch `currentUser` entity with `stripeCustomer.defaultPaymentMethod` relationship - Show correct form fields: - If there's a default payment method saved: show SavedCardDetails component. That component allows you to also select one-time payment instead (and optionally replace the current default payment method with this new payment card). If there's no default payment method saved, one-time payment form is shown on its own. - One-time payment needs card details, card holder's name etc. and optional permission to save card details for future bookings. - When requesting payment, a parameter needs to be passed if card details are saved at the same time: `setupPaymentMethodForSaving`. In the template, by default, this is handled in the first step of the _processCheckoutWithPayment_ function. - `stripe.confirmCardPayment` call, `confirm-payment` transition and the call to send initial message are handled as before - Save the payment method if user has given permission to do that: - Create a `stripeCustomer` entity for the current user if needed. - Attach the created payment method to `stripeCustomer` entity. Even if the call to save payment method fails, one-time payment itself has succeeded. Therefore, it is better to forward user to `TransactionPage` anyway. ### Getting permission to save a card Once you set up your payment flow to properly save a card with the Payment Intents or Setup Intents API, Stripe will mark any subsequent off-session payment as a merchant-initiated transaction to reduce the need to authenticate. Merchant-initiated transactions require an agreement (also known as a "mandate") between you and your customer. Read more about needed permissions from [Stripe's documentation](https://stripe.com/docs/payments/cards/reusing-cards#mandates). ## Charging Saved Cards If the user has a default payment method and she chooses to use it to book a listing, there are couple of changes needed: - The id of Stripe's payment method needs to be sent to Sharetribe API as `paymentMethod`, when requesting payment. - `stripe.confirmCardPayment`: Stripe Elements (card) is not needed if the default payment method is used. ## Saving Payment Card with PaymentIntents payment flow Here's the description of complete call sequence on CheckoutPage. PaymentIntents flow needs transaction process change as described in the article about **[payment intents](/concepts/payments/payment-intents/)**. ### Initial data for Checkout: Check if the user has already saved a default payment method. Fetch currentUser entity with `stripeCustomer.defaultPaymentMethod` relationship. In Sharetribe Web Template, we call a thunk function: [`fetchCurrentUser` in CheckoutPage.duck.js](https://github.com/sharetribe/web-template/blob/main/src/containers/CheckoutPage/CheckoutPage.duck.js#L409). Behind the scenes, this is essentially the following call: ```js sdk.currentUser.show({ include: ['stripeCustomer.defaultPaymentMethod'], }); ``` ### StripePaymentForm changes - show correct form fields: If there's a default payment method saved: show SavedCardDetails component. That component allows you to also select one-time payment instead (and optionally replace the current default payment method with this new payment card). If there's no default payment method saved, one-time payment form is shown on its own. One-time payment needs card details, card holder's name etc. and optional permission to save card details for future bookings. ### Submit StripePaymentForm After submitting `StripePaymentForm`, there are up to 5 calls in sequence (to Sharetribe and Stripe APIs): ##### Initiate transaction ```js sdk.transactions .initiate({ processAlias, transition: 'transition/request-payment', ...}) ``` What happens behind the scene: - API creates PaymentIntent against Stripe API and returns _stripePaymentIntentClientSecret_ inside the transaction's _protectedData_. - Booking is created at this step - so, availability management will block dates for conflicting bookings. - Automatic expiration happens in 15 minutes, if process is not transitioned with _transition/confirm-payment_ before that. - After this call, the newly created transaction is saved to session storage in Sharetribe Web Template (or existing inquiry transaction is updated). **When you intend to save card details**, a new parameter needs to be passed if card details are saved at the same time: `setupPaymentMethodForSaving`. ```js sdk.transactions.initiate({ processAlias, transition: 'transition/request-payment', params: { listingId, bookingStart, bookingEnd, setupPaymentMethodForSaving: true, }, }); ``` **When you are using previously saved payment card**, the id of Stripe's payment method needs to be sent to Sharetribe API as `paymentMethod`, when requesting payment. ```js sdk.transactions.initiate({ processAlias, transition: 'transition/request-payment', params: { listingId, bookingStart, bookingEnd, paymentMethod: stripePaymentMethodId, }, }); ``` In the default Sharetribe Web Template, both these cases are handled with _optionalPaymentParams_ in _CheckoutPageWithPayment.js_. ```jsx const optionalPaymentParams = selectedPaymentFlow === USE_SAVED_CARD && hasDefaultPaymentMethodSaved ? { paymentMethod: stripePaymentMethodId } : selectedPaymentFlow === PAY_AND_SAVE_FOR_LATER_USE ? { setupPaymentMethodForSaving: true } : {}; ``` Parameters might be different in different transaction process graphs. See the [`initiateOrderThunk` function](https://github.com/sharetribe/web-template/blob/main/src/containers/CheckoutPage/CheckoutPage.duck.js#L96) and related [`orderParams`](https://github.com/sharetribe/web-template/blob/main/src/containers/CheckoutPage/CheckoutPageWithPayment.js#L138) in the Sharetribe Web Template. ##### Confirm card payment ```js stripe.confirmCardPayment( stripePaymentIntentClientSecret, paymentParams ); ``` `paymentParams` should include card element and billing details. See [stripe.confirmCardPayment documentation](https://stripe.com/docs/js/payment_intents/confirm_card_payment).. PaymentParams are not needed when using a previously saved payment card. Sharetribe Web Template handles this in **[`confirmCardPayment` thunk function](https://github.com/sharetribe/web-template/blob/main/src/ducks/stripe.duck.js#L59)**. Stripe's frontend script checks if PaymentIntent needs extra actions from customer. Some payments might need Strong Customer Authentication (SCA). In practice, Stripe's script creates a popup (iframe) to card issuers site, where 3D secure v2 authentication flow can be completed. ##### Transition transaction ```js sdk.transactions.transition({ id: transactionId, transition: 'transition/confirm-payment', params, }); ``` Inform Marketplace API that PaymentIntent is ready to be captured after possible SCA authentication has been requested from user. Sharetribe Web Template does that in [`confirmPayment` thunk call](https://github.com/sharetribe/web-template/blob/main/src/containers/CheckoutPage/CheckoutPage.duck.js#L122) ##### Send message ```js sdk.messages.send({ transactionId: orderId, content: message }); ``` The template sends an initial message to the transaction if customer has added a message. ##### Save payment method As a final step, we need to save the payment method, if customer has selected the "Save for later use" checkbox. So, this is relevant if user has selected one-time payment - instead of making a charge from the previously saved credit card. In the template, we call [savePaymentMethod function](https://github.com/sharetribe/web-template/blob/main/src/ducks/paymentMethods.duck.js#L100) that creates stripe customer and adds updates default payment method. There are 3 different scenarios, which require different calls to Sharetribe API: **1. No StripeCustomer entity connected to Sharetribe API:** ```js sdk.stripeCustomer.create( { stripePaymentMethodId }, { expand: true, include: ['defaultPaymentMethod'] } ); ``` Template: [`dispatch(createStripeCustomer(stripePaymentMethodId))`](https://github.com/sharetribe/web-template/blob/main/src/ducks/paymentMethods.duck.js#L110) **2. Current user has already defaultPaymentMethod - 2 calls:** ```js => sdk.stripeCustomer.deletePaymentMethod({}, { expand: true }) => sdk.stripeCustomer.addPaymentMethod({ stripePaymentMethodId }, { expand: true }) ``` Template: [`dispatch(updatePaymentMethod(stripePaymentMethodId))`](https://github.com/sharetribe/web-template/blob/main/src/ducks/paymentMethods.duck.js#L112) **3. Current user has StripeCustomer entity connected, but no defaultPaymentMethod:** ```js => sdk.stripeCustomer.addPaymentMethod({ stripePaymentMethodId }, { expand: true }) ``` Template: [`dispatch(addPaymentMethod(stripePaymentMethodId))`](https://github.com/sharetribe/web-template/blob/main/src/ducks/paymentMethods.duck.js#L113) After these steps, the customer is redirected to their inbox. Depending on the transaction process, either the purchase is accepted directly, or it is up to the provider to accept or decline the booking. --- ## How Strong Customer Authentication works Path: template/payments/strong-customer-authentication/index.mdx # How Strong Customer Authentication works European regulation requires [Strong Customer Authentication (SCA)](https://stripe.com/guides/strong-customer-authentication) for online payments from European customers. This means that customers are sometimes asked to verify their purchases by an additional security layer called [3D Secure 2](https://stripe.com/gb/guides/3d-secure-2). Typically, this means that an online payment has to be verified via a customer’s online bank or mobile verification when conducting the payment. The default [transaction process](/concepts/transactions/transaction-process/) of Sharetribe and the [Sharetribe Web Template](https://github.com/sharetribe/web-template/) offer out-of-the-box support for SCA. This article clarifies how exactly transactions using SCA will work in the default process. ## How payment with SCA works From the customer's perspective, most transactions are business as usual: they make the payment normally by entering their credit card details, and that's it. However, on some occasions, they are presented with a popup that requires them to connect to their online bank to verify the transaction. The experience that follows depends on the bank of the customer. Each bank has their own user interface for this process. The example below is from a Finnish bank Osuuspankki. ![SCA in action](./sca_op_authentication.png 'SCA in action') At the point when the popup is presented, the transaction has already been initiated. If your marketplace is using availability management, the corresponding slots have been booked against the listing's availability calendar, and for stock listings, the relevant stock has been reserved. The provider already sees that a transaction has been initiated if they go to their inbox. They also notice that it's still pending payment verification. ![Pending payment](./sca_pending_payment.png 'Pending payment') By default, the provider doesn't get an email notification about the transaction yet at this point because the payment has not yet been completed. The customer now has 15 minutes to verify their transaction through their online bank. If this is done successfully, their credit card is preauthorized normally, and the provider is notified of an incoming order by email. If the customer fails to verify the transaction during the 15-minute timeframe, the transaction is automatically moved to the state "payment expired". At this point, the booking or stock reservation is canceled, and the booked time slots or reserved stock are freed. If the customer wanted to try the purchase again, they'd have to initiate a new transaction. Both parties will continue to see the expired booking in their inboxes. ![Payment expired](./sca_payment_expired.png 'Payment expired') ## What to do if SCA fails? If the customer fails to verify the transaction within the 15-minute timeframe, it's probably a good idea that you contact the customer in question to ask what went wrong. After all, they entered their credit card details and pressed the button to initiate the order, so they clearly want to go through with the booking or purchase. In Console, you can easily see all the transactions that failed because a payment expired. ![Expired payment in Console](./sca_console.png 'Expired payment in Console') If you want, you can also decide to notify the provider automatically by email if there's a failed payment. This allows the provider to go to their inbox and send a message to the customer to help them complete the order. ## Should you use SCA in your marketplace? If you want to be able to accept payments from European customers in your marketplace, your payment process must support SCA. Otherwise, payments done by European credit cards with a payment process that doesn't support SCA might fail. Even if you don't need to accept payments from European customers, it might still make sense for you to enable SCA for all transactions happening in your marketplace. The reason for this is that payments that have been successfully authenticated using 3D Secure are covered by a _liability shift_. From [Stripe documentation](https://stripe.com/docs/payments/3d-secure#disputed-payments): _"Should a 3D Secure payment be disputed as fraudulent by the cardholder, the liability shifts from you to the card issuer. These types of disputes are handled internally, do not appear in the Dashboard, and do not result in funds being withdrawn from your Stripe account."_ Credit card disputes can be costly to a marketplace. If disputes are causing you problems, enabling SCA can be a good idea. --- ## Performance and page speed Path: template/performance/how-to-improve-performance/index.mdx # Performance and page speed When we think about page speed, there are two different scenarios that we need to address: - The speed of the initial page load and possible reloads after that - The speed of changing the page within Single page application (SPA) The first one is usually a slower process. A browser needs to load all the HTML, CSS, JavaScript, and images - then it needs to understand and execute those files, calculate the layout, paint components and finally composite the whole view. The initial page load is the slowest since consequent reloads will benefit from caching. SPAs can improve from that since they don't necessarily need to download any more JavaScript, HTML, or CSS - already downloaded JavaScript might be enough to render consequent pages when a user navigates to another page. Most of the time, SPAs fetch data for that page. These two UX scenarios might also conflict with each other. If all the JavaScript is in one big bundle, page changes within a SPA are fast. However, downloading and evaluating a big JavaScript file slows initial page rendering. Even though users rarely experience the full initial page load speed when they use a SPA like the Sharetribe Web Template, it is good to keep track of that speed. Especially since that is what search engine bots monitor, it might affect your page rank. Read more about [website performance](https://developers.google.com/web/fundamentals/performance/why-performance-matters/). - [Measuring page performance](#measuring-page-performance) - [Optimize image sizes](#optimize-image-sizes) - [Lazy load off-screen images and other components](#lazy-load-off-screen-images-and-other-components) - [Use sparse attributes](#use-sparse-attributes) ## Measuring page performance You can use different online tools measure page performance. [Lighthouse](https://developers.google.com/web/tools/lighthouse/) and [PageSpeed Insights](https://pagespeed.web.dev/) are both popular tools to check rendering performance. Note that as the template is a single page application, a lot of the data associated with the application is loaded on the initial page load. This initial page load is also what is used for the Performance metric in Lighthouse and PageSpeed Insights. However, a Single Page Application is very fast after the initial page load, as most of the data needed to access consequent pages are already cached in the browser. While performance metrics can be a valuable tool to debug performance issues, they rarely consider users' subsequent actions on a website, operating under the assumption that users will only visit a single page on the website. We recommend using [code-splitting](/template/routing/code-splitting-in-template/) to improve initial page load times. You can further optimise performance by moving components only used on specific pages to page directories (with page directories being the directories you can find under `src/containers/`). Code located in the `src/components` directory is loaded into the `main.bundle.js` file served to the client on the initial page load. ## Optimize static image sizes If your page contains static images (e.g. images that are not served through Sharetribe), you should check that the image size is no bigger than the size it is rendered in. Adjusting image dimensions is the first step, but you should also consider image quality, advanced rendering options, and possibly serving those images from a CDN instead of from within your web app. This doesn't apply to images uploaded using Console; these images are optimized by our image delivery service. ## Lazy load off-screen images and other components Another way of dealing with images is to lazy load those images that are not visible inside an initially rendered part of the screen. Lazy loading these off-screen images can be done with a helper function: `lazyLoadWithDimensions` (from _util/contextHelpers/_). For an example how to use the helper function, see the [ListingCard component](https://github.com/sharetribe/web-template/blob/main/src/components/ListingCard/ListingCard.js). ## Use sparse attributes Another way to reduce the amount of data that is fetched from API is sparse attributes. This is a feature is not fully leveraged in the template, but it is created to reduce unnecessary data and speed up rendering. You can read more from [Marketplace API reference for sparse attributes](https://www.sharetribe.com/api-reference/#sparse-attributes). ## Use code splitting Code splitting is enabled with Loadable Components and by default route-based splits are made through _src/routing/routeConfiguration.js_. If you want to improve performance, you should prefer subcomponents inside page-directories instead of adding more code to shared components directory. Those components end up to main chunk file that is downloaded on each page (when full page-load is requested). You can read more [in the code splitting article](/template/routing/code-splitting-in-template/). ## Caching In some cases, introducing a basic caching mechanism on the server can help improve performance by avoiding repeated API calls for every request. The Sharetribe Web Template includes a lightweight example of this kind of caching in the sitemap generation logic. This caching uses an in-memory store with a time-to-live (TTL) value to keep data fresh for a set amount of time (in this case, one day): ```js const ttl = 86400; // 1 day in seconds const createCacheProxy = (ttl) => { const cache = {}; return new Proxy(cache, { get(target, property) { const cachedData = target[property]; if ( cachedData && Date.now() - cachedData.timestamp < ttl * 1000 ) { return cachedData; } return { data: null, timestamp: cachedData?.timestamp || Date.now(), }; }, set(target, property, value) { target[property] = { data: value, timestamp: Date.now() }; }, }); }; const cache = createCacheProxy(ttl); ``` This cache object can be used to temporarily store data like API responses or generated content. When a request is made, the server checks if the data is already cached and still valid and if so, it serves it instantly, skipping an call to the API. Keep in mind this approach works best for read-heavy, low-volatility data (such as content pages), and is not suitable for storing critical or user-specific data. --- ## Code splitting Path: template/routing/code-splitting-in-template/index.mdx # Code splitting ## What is code splitting Instead of downloading the entire app before users can use it, code splitting allows us to split code away from one main.bundle.js file into smaller chunks which you can then load on demand. To familiarize yourself with the subject, you could read about code splitting from [reactjs.org](https://reactjs.org/docs/code-splitting.html). In practice, the template uses route-based code splitting: page-level components use the [Loadable Components](https://loadable-components.com/) syntax to create [dynamic imports](https://webpack.js.org/api/module-methods/#import-1). ```js const AboutPage = loadable( () => import( /* webpackChunkName: "AboutPage" */ './containers/AboutPage/AboutPage' ) ); ``` When Webpack comes across these loadable objects, it will create a new JS & CSS chunk files (e.g. _AboutPage.dc3102d3.chunk.js_). Those code-paths are separated from the main bundle. ### Why should you use it? The main benefit of code splitting is reducing the code loaded for any single page. That improves the performance, but even more importantly, it makes it possible to add more navigational paths and page variants to the codebase. For example, adding different kinds of ListingPages for different listings makes more sense with code-splitting. Without code splitting, new pages, features, and libraries would impact the app's initial page load, and therefore SEO performance would drop too. Remember to keep non-reusable code in page-specific directories rather than in the src/components/ directory. This improves performance, as all the code in the shared directory is loaded in the main chunk file that is downloaded on each page. ## How code splitting works in practice Open the `/about` page. You will notice several JavaScript and CSS files loading: - **Main chunk** (e.g. _main.1df6bb19.chunk.js_ & main.af610ce4.chunk.css). They contain code that is shared between different pages. - [**Vendor chunk**](https://twitter.com/wSokra/status/969633336732905474) (Currently, it's an unnamed chunk file. e.g. _24.230845cc.chunk.js_) - **Page-specific chunk** (e.g. _AboutPage.dc3102d3.chunk.js_) There are several chunk files that can be loaded parallel in the first page-load and also page-specific chunks that can be loaded in response to in-app navigation. Naturally, this means that during in-app navigation there are now more things that the app needs to load: **data** that the next page needs and **code chunk** that it needs to render the data. The latter is not needed if the page-specific chunk is already loaded earlier. ### Preloading code chunks Route-based code splitting means that there might be a fast flickering of a blank page when navigation happens for the first time to a new page. To remedy that situation, the template forces the page-chunks to be [preloaded](https://loadable-components.com/docs/prefetching/#manually-preload-a-component) when the mouse is over **NamedLink**. In addition, **Form** and **Button** components can have a property `enforcePagePreloadFor`. That way the specified chunk is loaded before the user has actually clicked the button or executed form submit. ### Preloadable components in route configuration Preloadable components are defined in the routeConfigurations.js file: ```shell └── src └── routing └── routeConfiguration.js ``` At the beginning of the file, you can find the loadable component assigned to a constant variable. That variable is assigned to the `component` property of the corresponding route configuration: ```js // const AuthenticationPage = loadable(() => import(/* webpackChunkName: "AuthenticationPage" */ './containers/AuthenticationPage/AuthenticationPage')); { path: '/signup', name: 'SignupPage', component: AuthenticationPage, extraProps: { tab: 'signup' }, }, ``` #### Data loading The Sharetribe Web Template collects _loadData_ and _setInitialValues_ Redux functions from a [modular Redux file](https://github.com/erikras/ducks-modular-redux) (i.e. files that look like ``.duck.js). This happens in _pageDataLoadingAPI.js_: ```shell └── src └── containers └── pageDataLoadingAPI.js ``` Then those files can be connected with routing through route configuration. ```js // import getPageDataLoadingAPI from './containers/pageDataLoadingAPI'; // const pageDataLoadingAPI = getPageDataLoadingAPI(); { path: '/l/:slug/:id', name: 'ListingPage', component: ListingPage, loadData: pageDataLoadingAPI.ListingPage.loadData, }, ``` ### CSS chunk changes To ensure that every page-level CSS chunk has custom media queries included, those breakpoints are included through a separate file (_customMediaQueries.css_) and it is imported into the main stylesheet of every page. ```shell └── src └── styles └── customMediaQueries.css ``` ## Server-side rendering (SSR) When the template receives a page-load call on server and the page is a public one (i.e. the _"auth"_ flag is not set in route configuration), the server will render the page into a string and returns it among HTML code. This process has 4 main steps: 1. _server/dataLoader.js_ initializes store 2. It also figures out which route is used and fetches route configuration for it 3. If the configuration contains a _loadData_ function, the call is dispatched 4. As a consequence, the store gets populated and it can be used to render the app to a string. ### Build directory The sharetribe-scripts dependency uses Webpack to build the application. It copies the content from _public/_ directory into the _build_ directory and the Webpack build bundles all the code into files that can be used in production mode. - Code for server-side rendering is saved to _build/node_ directory. - Code for client-side rendering is saved to _build/static_ directory. - Both builds have also a _loadable-stats.json_ file, which basically tells what assets different pages need. - _server/importer.js_ exposes two [ChunkExtractors](https://loadable-components.com/docs/server-side-rendering/#collecting-chunks) - one for web and another for node build. - _server/index.js_ requires the entrypoint for the node build, extracts relevant info, and passes them to _dataLoader.loadData()_ and _rendered.render()_ calls. - webExtractor (ChunkExtractor for the web build) is used to collect those different code chunks (JS & CSS files) that the current page-load is going to need. In practice, the **renderApp** function wraps the app with _webExtractor.collectChunks_. With that, the webExtractor can figure out all the relevant loadable calls that the server uses for the current page and therefore the web-versions of those chunks can be included to rendered pages through `