Skip to main content

Creating and Registering Your App with Boulevard

A step-by-step guide to creating and installing a backend (offline) app

Written by Skya Jones

Before You Begin

How you get started depends on who you are:

  • If you're a developer building an app for a business: you'll need that business to create you a Boulevard Dashboard account first. This is the account you'll use to log in and submit your app.

    • The business will need to enable at least the following two permissions for your account:

      • Access login from any IP address

      • Access Dashboard

  • Manage Apps & Integrations

    • Only needed if the developer is responsible for actual application install

  • If you work at the business and are registering the app yourself: log in to your existing Boulevard Dashboard account as normal — no extra setup needed.

Note: This guide focuses on setting up an “offline” app, also called a machine-to-machine (M2M) app. This is the right setup for a person or team that does not have an OAuth server to leverage.

If you intend to create an app using an OAuth server — where a business owner is directed to an approval page instead, and your backend completes the handshake — you can skip ahead to “Setting Up an App With an OAuth Flow” near the end of this guide.

Creating and Installing Your App

Follow these steps in the Boulevard Dashboard:

  1. Log in to your Boulevard Dashboard account as normal.

  2. Navigate to the Manage Apps page on the Boulevard Developer Portal.

  3. Select “Create new app.”

  4. Submit your manifest. The manifest is a file that tells Boulevard two things: your app's name, and exactly what permissions (called “scopes”) it needs to do its job. See “About the Manifest File” below for a template you can start from.

  5. Once your manifest is submitted and your app is published, go back to your Boulevard Dashboard and open the “Apps & Integrations” page. Find your app and click “Install.”

Note: Custom apps are available to Enterprise-tier businesses only. If the business you're working with does not have an Enterprise license for Boulevard, they'll need to upgrade before your app can be installed. Business owners who want to upgrade to Enterprise should reach out to success@blvd.co for help.

About the Manifest File

The manifest for an application is a JSON file — a plain-text file with a specific structure that Boulevard reads. Think of it as your app's application form: it introduces your app by name and lists every piece of data and every action it's allowed to touch.

A few key parts of the manifest, in plain terms:

  • app.name and app.author: The name of your app and who made it. This is what the business will see when installing the app.

  • version.name: A version number for this manifest, like “1.0.0.” Each time you make changes, increase this number.

  • version.token_distribution: Set to “offline” for a backend/M2M app (the kind this guide covers).

  • required_scopes: The list of permissions your app is asking for, such as reading appointments or writing client information. Each one includes a short “justification” explaining why your app needs it.

  • webhook_events: Optional. If your app wants to register webhooks.

Note: You only need the scopes your app actually uses. A full template listing every available scope is included at the end of this guide (Appendix A) — delete anything you don't need, and fill in a real justification for anything you keep.

A Quick Look at the Structure

Here's what the shape of an offline manifest looks like, with just a couple of scopes as an example:

{
"manifest_version": 1,
"app": {
"name": "Application Name",
"author": "Author Name"
},
"version": {
"name": "1.0.0",
"token_distribution": "offline",
"description": "Application Description",
"required_scopes": [
{ "name": "appointment:read", "justification": "appointment:read-test" },
{ "name": "client:read", "justification": "client:read-test" }
]
}
}

The full downloadable template — with every available scope listed — is in Appendix A at the end of this guide.

Setting Up an App With an OAuth Flow

If you intend to create an app using an OAuth server rather than one that runs entirely on its own, you'll build an “online” app instead of an offline one. The overall registration steps above still apply (create the app, submit a manifest, install it), with two differences:

  • Your manifest includes an install_url and an oauth_redirect_urls value, and doesn’t set token_distribution to “offline.”

  • When a business owner clicks “Install,” they're directed to an approval page rather than a simple confirmation screen, and your server exchanges an authorization code for access and refresh tokens as part of the handshake instead of requesting tokens on demand.

If you already have an OAuth handler built or you're in the process of building one, this is the flow you'll want. Refer to your platform's Install Flow and Authentication documentation for the full details of that exchange.

Need Help?

  • If you have any issues or questions during development or app creation, please contact Developer Support at: dev-support@blvd.co.

  • For questions about upgrading to Enterprise tier: success@blvd.co

Appendix A: Full Offline (M2M) Manifest Template

This is a complete template for a complete manifest with all scopes included (referenced earlier above). Replace the placeholder values (app name, author, description, logo/icon URLs, etc.) with your own, and trim the required_scopes and webhook_events lists down to only what your app actually needs.

{
"manifest_version": 1,
"app": {
"name": "Application Name",
"author": "Author Name"
},
"version": {
"name": "1.0.0",
"token_distribution": "offline",
"description": "Application Description",
"description_markdown": "Description Markdown",
"logo_url": "URL",
"icon_url": "URL",
"required_scopes": [
{ "name": "appointment:client_message:read", "justification": "appointment:client_message:read-test" },
{ "name": "appointment:custom_field:read", "justification": "appointment:custom_field:read-test" },
{ "name": "appointment:custom_field:write", "justification": "appointment:custom_field:write-test" },
{ "name": "appointment:read", "justification": "appointment:read-test" },
{ "name": "appointment:write", "justification": "appointment:write-test" },
{ "name": "business:read", "justification": "business:read-test" },
{ "name": "business:write", "justification": "business:write-test" },
{ "name": "cart:read", "justification": "cart:read-test" },
{ "name": "cart:write", "justification": "cart:write-test" },
{ "name": "client:address:read", "justification": "client:address:read-test" },
{ "name": "client:address:write", "justification": "client:address:write-test" },
{ "name": "client:auth:manage", "justification": "client:auth:manage-test" },
{ "name": "client:custom_field:read", "justification": "client:custom_field:read-test" },
{ "name": "client:custom_field:write", "justification": "client:custom_field:write-test" },
{ "name": "client:dob:read", "justification": "client:dob:read-test" },
{ "name": "client:dob:write", "justification": "client:dob:write-test" },
{ "name": "client:email:read", "justification": "client:email:read-test" },
{ "name": "client:email:write", "justification": "client:email:write-test" },
{ "name": "client:notes:read", "justification": "client:notes:read-test" },
{ "name": "client:notes:write", "justification": "client:notes:write-test" },
{ "name": "client:phone:read", "justification": "client:phone:read-test" },
{ "name": "client:phone:write", "justification": "client:phone:write-test" },
{ "name": "client:pronoun:read", "justification": "client:pronoun:read-test" },
{ "name": "client:pronoun:write", "justification": "client:pronoun:write-test" },
{ "name": "client:read", "justification": "client:read-test" },
{ "name": "client:sex_assigned_at_birth:read", "justification": "client:sex_assigned_at_birth:read-test" },
{ "name": "client:sex_assigned_at_birth:write", "justification": "client:sex_assigned_at_birth:write-test" },
{ "name": "client:write", "justification": "client:write-test" },
{ "name": "credit_card:read", "justification": "credit_card:read-test" },
{ "name": "credit_card:write", "justification": "credit_card:write-test" },
{ "name": "custom_field:read", "justification": "custom_field:read-test" },
{ "name": "custom_field:write", "justification": "custom_field:write-test" },
{ "name": "form:read", "justification": "form:read-test" },
{ "name": "form:write", "justification": "form:write-test" },
{ "name": "gift_card:custom_field:read", "justification": "gift_card:custom_field:read-test" },
{ "name": "gift_card:custom_field:write", "justification": "gift_card:custom_field:write-test" },
{ "name": "gift_card:read", "justification": "gift_card:read-test" },
{ "name": "gift_card:write", "justification": "gift_card:write-test" },
{ "name": "location:read", "justification": "location:read-test" },
{ "name": "location:write", "justification": "location:write-test" },
{ "name": "marketing:read", "justification": "marketing:read-test" },
{ "name": "marketing:send", "justification": "marketing:send-test" },
{ "name": "membership_plan:read", "justification": "membership_plan:read-test" },
{ "name": "membership_plan:write", "justification": "membership_plan:write-test" },
{ "name": "membership:read", "justification": "membership:read-test" },
{ "name": "membership:write", "justification": "membership:write-test" },
{ "name": "offer:read", "justification": "offer:read-test" },
{ "name": "offer:write", "justification": "offer:write-test" },
{ "name": "order:read", "justification": "order:read-test" },
{ "name": "order:write", "justification": "order:write-test" },
{ "name": "package:read", "justification": "package:read-test" },
{ "name": "package:write", "justification": "package:write-test" },
{ "name": "product:read", "justification": "product:read-test" },
{ "name": "product:write", "justification": "product:write-test" },
{ "name": "purchase_order:read", "justification": "purchase_order:read-test" },
{ "name": "purchase_order:write", "justification": "purchase_order:write-test" },
{ "name": "referral:read", "justification": "referral:read-test" },
{ "name": "report:export", "justification": "report:export-test" },
{ "name": "report:read", "justification": "report:read-test" },
{ "name": "report:write", "justification": "report:write-test" },
{ "name": "secret:read", "justification": "secret:read-test" },
{ "name": "secret:write", "justification": "secret:write-test" },
{ "name": "service_rule:read", "justification": "service_rule:read-test" },
{ "name": "service_rule:write", "justification": "service_rule:write-test" },
{ "name": "service:custom_field:read", "justification": "service:custom_field:read-test" },
{ "name": "service:custom_field:write", "justification": "service:custom_field:write-test" },
{ "name": "service:read", "justification": "service:read-test" },
{ "name": "service:write", "justification": "service:write-test" },
{ "name": "shift:read", "justification": "shift:read-test" },
{ "name": "shift:write", "justification": "shift:write-test" },
{ "name": "sso:read", "justification": "sso:read-test" },
{ "name": "sso:write", "justification": "sso:write-test" },
{ "name": "staff:read", "justification": "staff:read-test" },
{ "name": "staff:write", "justification": "staff:write-test" },
{ "name": "tag:read", "justification": "tag:read-test" },
{ "name": "tag:write", "justification": "tag:write-test" },
{ "name": "timeblock:read", "justification": "timeblock:read-test" },
{ "name": "timeblock:write", "justification": "timeblock:write-test" },
{ "name": "timecard:read", "justification": "timecard:read-test" },
{ "name": "timecard:write", "justification": "timecard:write-test" },
{ "name": "voucher:read", "justification": "voucher:read-test" },
{ "name": "voucher:write", "justification": "voucher:write-test" },
{ "name": "webhook:read", "justification": "webhook:read-test" },
{ "name": "webhook:write", "justification": "webhook:write-test" }
],
"enable_business_app_update": true,
"webhook_events": [
"appointment.active", "appointment.arrived", "appointment.cancelled", "appointment.completed",
"appointment.created", "appointment.confirmed", "appointment.rescheduled", "appointment.updated",
"business.updated", "client.appointment_reminder_preferences_updated", "client.created",
"client.marketing_preferences_updated", "client.merged", "client.updated", "custom_form.submitted",
"entity_tags.added", "entity_tags.removed", "gift_card.balance_adjusted", "gift_card.created",
"location.created", "location.deleted", "location.updated", "mailer.appointment_cancellation",
"mailer.appointment_confirmation", "mailer.appointment_reminder", "mailer.appointment_staff_cancellation",
"mailer.appointment_staff_client_message", "mailer.appointment_staff_notification",
"mailer.membership_cancellation", "mailer.membership_pause", "mailer.membership_update",
"membership.cancelled", "membership.created", "membership.paused", "membership.reactivated",
"membership.renewal_failed", "membership.renewal_succeeded", "membership.unpaused",
"order.completed", "order.refund_closed", "ping", "product.quantity_adjustment_created",
"purchase_order.sent", "referral.created", "referral.deleted", "referral.reward_created",
"referral.updated", "report.export_completed", "scheduling.staff_shifts_changed",
"staff.created", "staff.deactivated", "staff.reactivated", "staff.restored", "staff.suspended",
"staff.updated", "timeblock.cancelled", "timeblock.created", "timeblock.rescheduled"
]
}
}
Did this answer your question?