Getting started
The Merchant API lets you manage your products, subscriptions, bookings, admin invites and door locks programmatically — the same capabilities the Backoffice uses, scoped to your merchant by an API key. You can build your own integration on top of it, including your own MCP server.
This page covers everything shared by every endpoint: the server URL, your API key, a one-request connectivity test, the conventions, and a ready-made Postman collection. The Integrations guide and Booking API pages then document the individual endpoints and assume the setup below.
Server URL
https://europe-west1-periode-prod.cloudfunctions.net/merchantApi
Every endpoint path in the docs is relative to this base URL. On the test
environment, replace periode-prod with periode-test.
Your API key (x-api-key)
Every request must send your merchant shared secret in the x-api-key
header:
x-api-key: <your-shared-secret>
Find and generate it under
Settings > Developer in
the Backoffice — the same screen shows your merchant ID, which is the
:id in every path. Your key only ever grants access to your own merchant;
requests that reference another merchant’s data are rejected as not found.
Keep the secret server-side. Anyone holding it has full API access to your merchant — including creating products and inviting admin users — so treat it as an admin-level credential: never ship it in a browser, mobile app, or public repository. If it leaks, regenerate it from the same screen.
Test your setup
Once you have the server URL, your merchant ID and your key, confirm all three line up with a single request:
GET /merchants/:id/ping
curl -H "x-api-key: <your-shared-secret>" \
https://europe-west1-periode-prod.cloudfunctions.net/merchantApi/merchants/<your-merchant-id>/ping
On success it returns 200 with the merchant your key resolves to:
{ "ok": true, "merchantId": "<your-merchant-id>", "name": "Your merchant" }
A 401 means the key (or header) is wrong; a different name than you expect
means the merchant ID is wrong.
Conventions
- All request and response bodies are JSON.
:idin a path is always your merchant ID.- Monetary amounts are in minor units (cents):
49000= 490.00. - Booking durations are in decimal hours (
1.5= 1h30m). - VAT on booking and punch-pass products is a fraction between
0and1(0.25= 25%). On subscriptions it is the string fieldmva(e.g."25%"). - Whole-field replace on updates: every key you send replaces that field entirely. To change one element of an array, send the full new array.
currencyis always taken from your merchant account — never send it.paymentTypeshould be["ADYEN"]on merchants that are set up with Adyen — which is every new merchant. See the section below.- On success, endpoints return
200. Schema validation errors return400with a body naming the offending fields (amessageplus per-fielddetails). Every other failure — a product that doesn’t exist, a duplicate id, a payment provider error — also returns400but with an empty body ({}), so branch on the status code rather than on a reason. A resource that doesn’t exist or belongs to another merchant is rejected this way deliberately: the response never reveals whether it exists or who owns it.
Payment types (paymentType)
Products carry a paymentType array (supportedPaymentMethods on
subscriptions). The three values are not three equal choices — ADYEN is
the current platform, while VIPPS and STRIPE are separate, older payment
tracks with their own credentials.
Use ["ADYEN"]. Adyen’s checkout already offers Vipps, card and the other
methods you have enabled under Settings > Payment methods > Adyen, so a single
["ADYEN"] gives your customers Vipps without naming it here.
Do not send ["VIPPS"] on an Adyen merchant. That value selects the
standalone Vipps integration, which needs its own Vipps merchant serial number
(merchantSerialNumber). A merchant that doesn’t have one cannot create a
payment session at all: the booking fails at checkout with
400 Failed creating session, and — because it fails when the customer pays,
not when you save the product — the product itself looks perfectly fine in the
Backoffice. The Backoffice picks ADYEN for you, so this is a mistake only
reachable through the API.
One exception — subscriptions. Adyen covers Vipps for one-off payments but not for recurring subscription charges. If a merchant needs Vipps recurring, that still requires the standalone Vipps track and a
merchantSerialNumber. For every other product type — booking, gift card, punch pass, single payment —["ADYEN"]is the answer.
STRIPE is used only by merchants explicitly onboarded to Stripe.
Postman collection
Import this collection to get every endpoint pre-built. In Postman choose
Import > Raw text, paste the JSON below, then open the collection’s
Variables tab and set merchantId to your merchant ID and apiKey to your
shared secret. The collection ships without a key — you fill it in once, and
every request sends x-api-key: {{apiKey}} automatically.
{
"info": {
"name": "Periode Merchant API",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"auth": {
"type": "apikey",
"apikey": [
{ "key": "key", "value": "x-api-key" },
{ "key": "value", "value": "{{apiKey}}" },
{ "key": "in", "value": "header" }
]
},
"variable": [
{ "key": "baseUrl", "value": "https://europe-west1-periode-prod.cloudfunctions.net/merchantApi" },
{ "key": "merchantId", "value": "" },
{ "key": "apiKey", "value": "" }
],
"item": [
{
"name": "Ping",
"request": { "method": "GET", "url": "{{baseUrl}}/merchants/{{merchantId}}/ping" }
},
{
"name": "List all products",
"request": { "method": "GET", "url": "{{baseUrl}}/merchants/{{merchantId}}/allProducts" }
},
{
"name": "Get a product",
"request": { "method": "GET", "url": "{{baseUrl}}/merchants/{{merchantId}}/products/subscription/:productId" }
},
{
"name": "Create subscription product",
"request": {
"method": "POST",
"url": "{{baseUrl}}/merchants/{{merchantId}}/products/subscription",
"body": {
"mode": "raw",
"options": { "raw": { "language": "json" } },
"raw": "{\n \"interval\": \"MONTH\",\n \"intervalCount\": 1,\n \"price\": 49000,\n \"productName\": \"Monthly membership\",\n \"productDescription\": \"Access to everything\",\n \"scopes\": [\"email\", \"name\"],\n \"maxNumber\": null,\n \"chooseNumber\": null\n}"
}
}
},
{
"name": "Update product",
"request": {
"method": "PUT",
"url": "{{baseUrl}}/merchants/{{merchantId}}/products/subscription/:productId",
"body": {
"mode": "raw",
"options": { "raw": { "language": "json" } },
"raw": "{\n \"interval\": \"MONTH\",\n \"intervalCount\": 1,\n \"price\": 59000,\n \"productName\": \"Monthly membership\",\n \"productDescription\": \"Access to everything\",\n \"scopes\": [\"email\", \"name\"],\n \"maxNumber\": null,\n \"chooseNumber\": null\n}"
}
}
},
{
"name": "Create gift card",
"request": {
"method": "POST",
"url": "{{baseUrl}}/merchants/{{merchantId}}/products/gift-card",
"body": {
"mode": "raw",
"options": { "raw": { "language": "json" } },
"raw": "{\n \"name\": \"Gift card\",\n \"description\": \"A gift card\",\n \"paymentType\": [\"ADYEN\"],\n \"buyerScopes\": [\"email\"],\n \"priceChoices\": [{ \"price\": 50000 }]\n}"
}
}
},
{
"name": "Create punch pass",
"request": {
"method": "POST",
"url": "{{baseUrl}}/merchants/{{merchantId}}/products/punch-pass",
"body": {
"mode": "raw",
"options": { "raw": { "language": "json" } },
"raw": "{\n \"name\": \"Ten pack\",\n \"description\": \"Ten sessions\",\n \"bookingProducts\": [{ \"bookingId\": \"<booking-id>\", \"cost\": 1 }],\n \"choices\": [{ \"numberOfPunches\": 10, \"amount\": 200000 }],\n \"vat\": 0\n}"
}
}
},
{
"name": "Invite admin user",
"request": {
"method": "POST",
"url": "{{baseUrl}}/merchants/{{merchantId}}/invites",
"body": {
"mode": "raw",
"options": { "raw": { "language": "json" } },
"raw": "{\n \"email\": \"admin@example.com\",\n \"roles\": [\"admin\"],\n \"sendEmail\": false\n}"
}
}
},
{
"name": "Create lock system",
"request": {
"method": "POST",
"url": "{{baseUrl}}/merchants/{{merchantId}}/lockSystems",
"body": {
"mode": "raw",
"options": { "raw": { "language": "json" } },
"raw": "{\n \"name\": \"Front door\",\n \"lockSystem\": { \"type\": \"STATICPIN\", \"pinCode\": \"1234\" }\n}"
}
}
},
{
"name": "List lock systems",
"request": { "method": "GET", "url": "{{baseUrl}}/merchants/{{merchantId}}/lockSystems" }
},
{
"name": "Add lock secret",
"request": {
"method": "POST",
"url": "{{baseUrl}}/merchants/{{merchantId}}/lockSecrets",
"body": {
"mode": "raw",
"options": { "raw": { "language": "json" } },
"raw": "{\n \"provider\": \"inlet\",\n \"name\": \"main-door\",\n \"key\": \"<provider-api-key>\"\n}"
}
}
},
{
"name": "List lock secrets",
"request": { "method": "GET", "url": "{{baseUrl}}/merchants/{{merchantId}}/lockSecrets" }
}
]
}