# 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:
---
## 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.

## 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.

### 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.

### 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.

### 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.

## 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.

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:

## 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:

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:

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:

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.

## 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.

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
```

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
```

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}}
```

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
```

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
}}
```

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,
}
);
```

```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
}}
```

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.

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.

**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.

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.

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.

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.

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

## 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

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.

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

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

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.

#### 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.

## 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.

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.

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.

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).

## 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

#### 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.

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.

- Customer commission is defined as **positive**, since the customer's
total is the listing price plus the customer commission.

## 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,
},
];
```

### 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,
},
];
```

### 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'],
},
]
: [];
```

### 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

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.

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.

## 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_

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_.

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.

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.

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

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_

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.

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_

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_

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_.

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_

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.

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.

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.

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 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/)

```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.

```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.

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.

```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.

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.

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.

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:

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 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:

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:

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**:

Remember to also rename all files within the new directory from
**SectionArticle** to **SectionArticleAlignLeft**:

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:

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 "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:

It will also output the rendered text version to the terminal:
{/*  */}
```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:

## 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:
{/*  */}
```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:

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:

## 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.

---
## 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.

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.

---
## 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:

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%:

---
## 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:

{/* 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.

{/* 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.

{/* 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.

{/* 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:

{/* 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.

---
## 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.

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.

## 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.

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.

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.

```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.

## 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`.

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:

{}
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:

{}
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..."

{
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 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.

### 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.

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

## [ 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).

Click the `Add new` link, fill in an application name (for instance "My
example integration") and choose `Integration API` as the API.

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.

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_

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.
{
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

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.

## 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:

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

### 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.

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 2:**
Both timeslots are returned by the query.

**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 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.

## 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.

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 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`.

## 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.

## 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:

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.

### 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.

## 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.

## 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:

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:

## 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:

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:

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:

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