Docs menu and search Getting started

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.
  • :id in 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 0 and 1 (0.25 = 25%). On subscriptions it is the string field mva (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.
  • currency is always taken from your merchant account — never send it.
  • paymentType should 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 return 400 with a body naming the offending fields (a message plus per-field details). Every other failure — a product that doesn’t exist, a duplicate id, a payment provider error — also returns 400 but 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" }
    }
  ]
}