# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
This site has an agentic ask interface you may use to query the documentation dynamically by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://api-how-to.plannrcrm.com/index.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

---

# Welcome!

Introduction

Welcome to the Plannr API guide! Here you will find all the documentation you need to get up and running with Plannr API. Have fun!

## Get Started with Plannr

Before you start using the API, you will need to complete some necessary steps to get going. You'll also find useful links to help you as you consume our API. It is highly recommended to start here - you'll start at the very beginning by authorising yourself and end with making an example request to the API:

{% content-ref url="/pages/y6myHLmGygQAweJTprLc" %}
[Authentication](/getting-started/authorization)
{% endcontent-ref %}

## Want to deep dive?

Dive a little deeper and start exploring our API reference to get an idea of everything that's possible with the API:

[https://apidocs.plannrcrm.com](https://apidocs.plannrcrm.com/)


# Authentication

Getting access to our API

Currently, Plannr offers two ways of authentication - both of which utilise Bearer Tokens for authenticating requests that you make. You can authenticate with our API in two ways.

## Personal Access Token (Bearer)

> Recommended for: Personal Use

This type of authentication can only be used with your User on Plannr and all Accounts that you are associated with. This token cannot be shared with other end-users and should only be used for personal access to the API - for example, when building an integration for your own use. You can create personal access tokens yourself by logging into Plannr and visiting Settings, Account Details and scrolling down to "Personal Access Tokens".&#x20;

## OAuth2 Authorization Code Grant

> Recommended for: Integrating an app with Plannr. Sign in with Plannr. Using Plannr's data.

This type of authentication is suited for other applications creating integrations for Plannr with their software. With OAuth2, you will be able to request access to any of Plannr's Users after they have given you permission to access their data.

See this useful article from Okta (note - we are not affiliated nor use Okta's services): <https://developer.okta.com/blog/2018/04/10/oauth-authorization-code-grant-type>

#### Getting access to our API using OAuth2&#x20;

Currently, there is no automated process to gain access to the Plannr API with OAuth2, if you are building an integration for yourself or you are an agency working on behalf of one of our clients, please use personal access tokens, however if you would like to use OAuth2 please follow the instructions below.

To get access, please email [**integrations@plannrcrm.com**](mailto:integrations@plannrcrm.com) and provide the following information:

* Your Name
* Company Name
* Redirect URLs needed (For example <https://myapp.com/auth/plannr/callback>)

## Authentication

### With Personal Access Token

After you have been given your personal access token you can use it to authenticate with Plannr by passing it into the `Authorization` header with the prefix of "Bearer". For example:

* Header Name: `Authorization`
* Header Value: `Bearer YOUR-PERSONAL-ACCESS-TOKEN-HERE`

Example cURL Request:

```
curl
--location 'https://api.plannrcrm.com/api/v1/logins' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <YOUR-TOKEN-HERE>'
```

### With OAuth2 Authorization Code Grant

Authenticating using the OAuth2 Authorization Code Grant is slightly different. Plannr follows the OAuth2 standard so this documentation assumes you have knowledge of how this authentication method works.&#x20;

Once you contact Plannr, one of the team will provide you with a **Client ID** and **Secret**.

{% hint style="danger" %}
Never expose your provided secret to the public. Store it securely and limit access.
{% endhint %}

The OAuth authorization URL will only accept redirect URLs that you provide to Plannr. If you need any additional URLs approved, please email Plannr.

Plannr's OAuth2 server is on the same domain as the rest of the API. These are the following URLs you need:

* <https://api.plannrcrm.com/oauth/authorize> (For redirecting your users to get access to Plannr)
* <https://api.plannrcrm.com/oauth/token> (For requesting an API access or refreshing existing access tokens)
* Scopes just send: `*`

#### Example OAuth2 Authorization Code Flow

First, redirect your users to:

```uri
https://api.plannrcrm.com/oauth/authorize?response_type=code&client_id=CLIENT-ID&scope=*&redirect_uri=REDIRECT-URI&state=RANDOM-UNIQUE-STRING
```

Once users approve your application, they will be redirected to the redirect\_uri where you will be given a "code" as a query parameter. You should also verify that the state that you included as a query parameter is the same in this stage to prevent man-in-the-middle attacks.

Next, make a POST request to [https://api.plannrcrm.com/oauth/token](https://api.plannrcm.com/oauth/tokens) with the Content-Type of `application/x-www-form-urlencoded` and you want to send the following properties:&#x20;

* grant\_type: "authorization\_code"
* client\_id: CLIENT-ID
* client\_secret: CLIENT-SECRET
* redirect\_uri: REDIRECT-URI
* code: CODE-RECEIVED

```
curl \
--request POST \
--location 'https://api.plannrcrm.com/oauth/token' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=authorization_code&client_id=X&client_secret=X&redirect_uri=X&code=X'
```

After that, you will be given an access\_token, refresh\_token and an expiry date. The refresh token will expire after 6 months and the user will have to do steps 1-3 again.&#x20;

To refresh an access token after it expires you can make the following POST request to: [https://api.plannrcrm.com/oauth/token](https://api.plannrcm.com/oauth/tokens).

```
curl \
--request POST \
--location 'https://api.plannrcrm.com/oauth/token' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=refresh_token&client_id=X&client_secret=X&refresh_token=X'
```

After you have completed the authentication process you can use the access token provided to make API calls. For example:

```
curl \
--location 'https://api.plannrcrm.com/api/v1/logins' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <YOUR-TOKEN-HERE>'
```

## [Postman](https://www.postman.com/) Collection Sample

We have created a very basic Postman collection to get up and running with authentication. You can use the variables in the collection to store your Client ID and Secret.

{% file src="/files/wzLsjI4GNNag0IOwFypu" %}
Postman example collection
{% endfile %}

## Recommended Reading

It is recommended to read the [Making Requests](/getting-started/making-requests) section to understand how to build a request to Plannr's API. The [Users, Logins, and Accounts](/getting-started/accounts) section also explains how the API handles end-users and what Accounts the requests are made for.


# Making Requests

Structuring a request to Plannr's API

{% hint style="info" %}
Update from 29th November 2024 - Plannr now offers a new field "preferred\_account\_uuid" which can be used, however if you have built your integration before this date, you will not be enrolled in the new authentication. Please get in touch with <integrations@plannrcrm.com> if you would like your integration to use this new "Preferred Account" specified by users.
{% endhint %}

## Getting the X-PLANNR-ACCOUNT-UUID header

Most of Plannr's API endpoints support multi-tenancy. This means a UUID must be specified for the account that you want to access data for.  This UUID must be passed into a header called `X-PLANNR-ACCOUNT-UUID`, and API requests that require it will throw an error if you do not provide it.

You can get the `X-PLANNR-ACCOUNT-UUID` header value by making a GET request to [`https://api.plannrcrm.com/api/v1/logins`](https://apidocs.plannrcrm.com/operation/operation-get-api-v1-account-parameter-login) with your access token retrieved in the step before. This endpoint will return a list of **logins** that the user you have connected with has, and you can iterate through these logins, and access the **account->uuid** to get the `X-PLANNR-ACCOUNT-UUID`

If you have built your integration after 29th November 2024, there is an additional field in this API response called `preferred_account_uuid`, which we recommend using, as this is the account they have requested you to use. Otherwise, you should iterate through each of the accounts and use the one that is most suitable. Please note that some users may have an admin account and a client account on one user, so be cautious which one you select.

{% hint style="warning" %}
Please note the **api/v1/logins** route will return logins, with accounts within each login object, you must use the **uuid** field within the **account** object of each login for this. to work.
{% endhint %}

## Required Request Headers

Whenever you make a request to the Plannr API, there are a number of required headers you **MUST** send with the request:

1. `Accept: application/json`
2. `Content-Type: application/json`
3. `Authorization: Bearer YOUR-ACCESS-TOKEN`
4. `X-PLANNR-ACCOUNT-UUID: ACCOUNT-UUID` (On most API calls, not all)


# Users, Logins, and Accounts

How end-users use and access Plannr

## Introduction

It is imperative to understand the difference between Users, Accounts and Logins within the context of Plannr, as they are all separate models that work in conjunction with each other. This section aims to explain the structure of the Accounts system.&#x20;

### What is a Firm?

A Firm in the financial world is essentially a business. Since Plannr CRM is a tool for financial advisers, anyone that uses Plannr will belong to a Firm. Firms are the hubs for everything on Plannr, containing vast amounts of models representing different areas of the business in the real world such as Accounts for Clients/Employees, Business Reports and Plans.

In an API context within Plannr, many models are stored and authenticated against a Firm. Through relationships, most models belong to a Firm one way or another. For example, a Folder for Files belongs to an Account, which in turn belongs to a Firm.

## Models

### Users

The User model represents an end-user on a Firm in Plannr. A User has their own details, like an email address, password, and name. The email address and password on a User model are always what an end-user logs in to Plannr with, even if they have multiple different logins across different Firms.

### Logins

A Login is the middleman between a User and an Account. Logins store permissions and are related to both an Account and a User - a Login can only belong to one of each in a One to One relationships. Users can have many logins for different Accounts and Accounts themselves can also have many logins which would correspond to different users.&#x20;

### Accounts

An Account is where the information representing a person or entity on a Firm is contained. This includes details like the Account type (client, employee etc.). Accounts can have many Logins which can allow various different Users to use that Account.&#x20;

For example, a Joint account representing two people may have a Login for each participant, which in turn would correspond to different Users. If an Account with an email address has a Login created for it, an email is sent to that address if the Login does not match an existing user. This email prompts an end-user to register - creating a new associated User model. Otherwise, if a User with the same email address already exists upon creation, the Login will be associated to that User.

Accounts also have types and roles which represent various financial entities within a Firm. In the context of Employees, there is a hierarchy of increasing permissions with the highest being Owner and lowest being a Member. This table explains which types correspond to which role:

| Type     | Roles                              |
| -------- | ---------------------------------- |
| Client   | Individual, Trust, Business, Joint |
| Employee | Owner, Admin, Member               |
| External | Introducer                         |

## Structure and Relationships

Let's start with a diagram that shows a good example of two different Users with multiple Logins and Accounts:

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FW7FnEg3Yc4gMDTDV4A3i%2Faccounts.png?alt=media&amp;token=2d1257c9-f4c4-4675-8a7e-e8c407a6b455" alt=""><figcaption><p>An example of two User Models with access to multiple related Logins and Accounts</p></figcaption></figure>

There are a few key rules and relationships that can be learned here:

* Users can have many Logins (HasMany)
* Accounts can have many Logins (HasMany)
* Logins belong to only one Account and one User (OneToOne)
* Users can have Logins that correspond to Accounts on differing Firms
* Different Users can access the same Account as long as they have a Login for it

## API Perspective

Let's examine this structure from a perspective of a developer consuming the API - this is important as there are a few key considerations.&#x20;

### Logins Endpoint

First is an important endpoint that isn't currently listed in the API Documentation:

`https://api.plannrcrm.com/api/v1/logins`

This route will list all logins and there are no other headers required, you just have to pass the Bearer Token for access.

### Initial Setup

Currently, a Firm cannot be setup via the API, so it will always have at least one initial User with a Login and Account.

When creating and authenticating a User for the first time make an API call to the endpoint listed above. It is recommended when building an integration for the Plannr API to take the first login from the response and access the related Account to get the UUID. As mentioned in the [Making Requests](/getting-started/making-requests) section, the `X-PLANNR-ACCOUNT-UUID` is always required for every other endpoint as the API is stateless!


# Using The API Reference

The top of this User Guide contains a link to the documentation for Plannr API which details all possible endpoints that can be consumed. The API follows a typical REST architecture, with most endpoints returning a JSON resource.

{% hint style="info" %}
[📄API Documentation](https://apidocs.plannrcrm.com/)
{% endhint %}

Endpoints are grouped based on the area of Plannr that they affect. Each endpoint has a tag for what type of HTTP request it needs to be, as well as a small description to describe what the endpoint does.

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FIul3LfEUGgUANoAv0Fpp%2Fimage.png?alt=media&amp;token=92ec5c4c-5ce7-40ec-bcc0-7e355403a0e6" alt=""><figcaption><p>An example of a group of endpoints in the side navigation menu</p></figcaption></figure>

Upon clicking an endpoint in the navigation bar, the documentation will display the endpoint question and all relevant information for that endpoint.

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FKk3hdIyKppfTAuQxkNjD%2Fimage.png?alt=media&amp;token=65e23706-97ee-4681-babd-0c1d115f4b40" alt=""><figcaption><p>An example of the main page for a specific endpoint</p></figcaption></figure>

The search bar can also be utilised to search for specific endpoints, parameters, or response data.


# Query Parameters

Control your request's results

## Pagination

Plannr has pagination built into the API and it is incredibly easy to use. Unlike some APIs, there is no need to manually define a `limit` or `offset`.&#x20;

For endpoints where pagination is possible (usually index endpoints) - all that is required is an integer query parameter called **`per_page`**. This parameter controls the number of results to return per page. For example, proving a `per_page` parameter of 30 to the 'Get All Accounts' endpoint will return 30 accounts objects per page:

`https://api.plannrcrm.com/api/v1/account?per_page=30`\
\
Request pagination can also be combined with filters and sorts to further control the results in the returned resource collection:\
\
`https://api.plannrcrm.com/api/v1/account?sort=first_name&per_page=30`

It is important to note that any endpoint with the ability to paginate will have a default value for results per page, as well as a limit to the parameter. These values are displayed on the documentation page for each endpoint that can accept a `per_page` parameter. The usual default is 15 results per page, with the limit being 500.

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FE39hdinLhPe0pX68TwC7%2Fimage.png?alt=media&amp;token=3f1fc338-4cdf-4214-b209-0ebca478378e" alt="" width="563"><figcaption><p>An example of the per_page parameter on an endpoint in the API documentation</p></figcaption></figure>

### Links and Meta

Paginated responses from the Plannr API also include two other keys alongside Data: Links and Meta.&#x20;

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FeieZTUXbBqaucE5Q5hat%2FScreenshot%202023-09-13%20at%2015.06.53.png?alt=media&amp;token=ba3930b0-2be9-4c1a-9730-c48f7702257a" alt=""><figcaption><p>An example of the Links and Meta keys in a returned response on the Get All Plans endpoint</p></figcaption></figure>

The Links block contains URL links to various pages in the response. The 4 keys present in this block can be seen above. The provided links in this section may be useful if it is necessary to have a specific link to a certain page in a response. For example, when building an integration, you may wish to build an interactable button that take an end-user to the next page of results.

The Meta block provides useful metadata about the response, it's results, and the URL links for each page. The key contains information such as how many results are displayed per page in the response, and how many returned objects there are in total. Metadata about the current page, which page was accessed last and the last page. Similiar to Links, this data may be required when building an integration, and can be found alongside the normal return objects in each paginated response.

## Sort

Many endpoints in the API include the ability to send a sort query parameter, enabling items in the returned resource collection to be sorted based on the provided parameter. Only one sort query parameter can be provided for a request.

The available valid fields for a sort query parameter vary based on the endpoint. Check the API documentation to see what can be provided for each endpoint.

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FdR9Pm1ygcitLJVlIgUjx%2Fimage.png?alt=media&amp;token=0f319a7e-a6bd-4c31-a41e-cb545ea55c7f" alt="" width="563"><figcaption><p>An example of the valid fields for the sort query parameter on the 'Get All Accounts' endpoint</p></figcaption></figure>

Many endpoints will have a default sort and providing a different sort parameter will override this. A negative sign (-) can be prefixed to any sort parameter to retrieve results in descending order instead of ascending.

An example of an endpoint with a sort query parameter provided:

`https://api.plannrcrm.com/api/v1/account?sort=first_name`

This will return all accounts in ascending alphabetical order based on the `first_name` field of each account.

## Filters

Filter query parameters are a powerful tool for customising the response from a request. The returned collection can be filtered to only include certain objects with data that corresponds to the filter. Multiple filters can be used together in a request. Plannr API has many filters available on certain endpoints (indexes) that can be used in conjunction with each other as well as a sort and pagination.

A filter parameter always begins with `filter[]`, with the field to filter by contained in the square brackets e.g. `filter[name]`.

The available fields and their corresponding valid values are documented per endpoint, in a similar manner to the `sort` query parameter. Unlike a `sort`, a filter will remove results from the resource entirely.

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FPL5aV2vIdXgLzFuiOlnF%2Fimage.png?alt=media&amp;token=477b4e61-30d9-440b-901f-74123cb6abe7" alt="" width="375"><figcaption><p>An example of available filters for the 'Get All Accounts' endpoint</p></figcaption></figure>

Filter parameters are provided like other query parameters,and as mentioned previously, can be used in conjuction with all other types of parameters:

`https://api.plannrcrm.com/api/v1/account?filter[type]=external&sort=first_name&per_page=8`

## Includes

Many endpoints in the API allow includes to be added to the query to load relationships. Using an include against an endpoint will load related models and their data alongside the base query.\
\
In the responses from the API, examples of all the relationships loaded against a particular response are shown. For example, a client with their assigned adviser:

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FHTi1z0wJosWBI818tjj9%2Fimage.png?alt=media&amp;token=5db12e52-47d8-41cb-8d74-b3b8b7fafe63" alt=""><figcaption><p>An example of a loaded relationship under the assigned_adviser key in a response from the Get an Account endpoint</p></figcaption></figure>

However, this does not mean the relationship is always loaded in the response. Most 'show' routes with load every relationship, but on 'index' routes you will have to define the includes. Any available includes are listed in the API documentation:

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FcMTE38qEdoqbKZXKQMyY%2Fimage.png?alt=media&amp;token=a41598e6-5dcc-4a7e-96f2-dd425bf94ecb" alt=""><figcaption><p>The allowed query includes on the Get All Accounts endpoint</p></figcaption></figure>


# Rate Limits

Plannr's API includes rate limiting to prevent bad actors from overloading the infrastructure and causing a loss of quality for our customers. We offer a generous API rate limit of 500 requests per minute per user. If you get errors with the status code of 429 "Too Many Requests" please make sure you implement a back-off strategy until the rate limit has been lifted.&#x20;

There is a useful `X-Ratelimit-Remaining` header provided in every response from the API which you can programmatically hook up to prevent being rate-limited.


# Validating Webhook Signatures

How to verify that a webhook was genuinely sent by PlannrCRM

Every webhook request sent by Plannr includes an `X-Signature` header. This is an HMAC-SHA256 signature of the raw request body, computed using your webhook subscription's `signing_secret`. By verifying this signature before processing a webhook, you can be confident the payload was sent by Plannr and has not been tampered with in transit.

***

## Step 1 — Retrieve your signing secret

A unique 60-character `signing_secret` is automatically generated when a webhook subscription is created. It is returned in the response body whenever you create, update, or retrieve a webhook subscription through the API.

{% hint style="info" %}
The signing secret is not currently displayed in the Plannr UI. Retrieve it via the API and store it securely in your application.
{% endhint %}

Make a `GET` request to retrieve all of your webhook subscriptions and their secrets:

```
GET /api/v2/webhook-subscriptions
```

Example response:

```json
{
  "data": [
    {
      "uuid": "53f22fe4-3731-4cdc-aa12-e65bd0b15e52",
      "created_at": "2024-01-15T10:30:00+00:00",
      "updated_at": "2024-01-15T10:30:00+00:00",
      "signing_secret": "AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKlMnOpQrSt",
      "url": "https://api.example.com/webhooks",
      "events": ["account.created", "client.updated"],
      "last_outgoing_webhook_call_at": "2024-03-01T14:22:11+00:00"
    }
  ]
}
```

{% hint style="danger" %}
Treat your `signing_secret` like a password. Never expose it in client-side code, logs, or version control. Store it securely using environment variables or a secrets manager.
{% endhint %}

***

## Step 2 — Understand the signature

When Plannr sends a webhook to your endpoint, the request will include:

* **`X-Signature`** — an HMAC-SHA256 hex digest of the raw request body, keyed with your `signing_secret`
* **`Content-Type: application/json`** — the body is a JSON payload

Example headers received on your endpoint:

```
POST /webhooks HTTP/1.1
Content-Type: application/json
X-Signature: 3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4
```

***

## Step 3 — Verify the signature

To validate the webhook, compute your own HMAC-SHA256 signature of the **raw request body** using your `signing_secret`, then compare it against the `X-Signature` header. If they match, the webhook is genuine.

{% hint style="warning" %}
Always use the **raw, unparsed request body** when computing the signature — not a re-serialised version of the parsed JSON. Parsing and re-serialising can alter whitespace or key ordering, causing the signature check to fail.
{% endhint %}

{% tabs %}
{% tab title="Python" %}

```python
import hmac
import hashlib

raw_body = request.body
x_signature = request.headers.get("X-Signature", "")
signing_secret = "your-signing-secret-here"

expected = hmac.new(
    key=signing_secret.encode("utf-8"),
    msg=raw_body,
    digestmod=hashlib.sha256,
).hexdigest()

if expected != x_signature:
    raise Exception("Invalid signature")

payload = json.loads(raw_body)
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const crypto = require("crypto");

const rawBody = req.body; // use express.raw() middleware
const xSignature = req.headers["x-signature"];
const signingSecret = "your-signing-secret-here";

const expected = crypto
  .createHmac("sha256", signingSecret)
  .update(rawBody)
  .digest("hex");

if (expected !== xSignature) {
  throw new Error("Invalid signature");
}

const payload = JSON.parse(rawBody.toString("utf8"));
```

{% endtab %}

{% tab title=".NET (C#)" %}

```csharp
using System.Security.Cryptography;
using System.Text;

var rawBody = await new StreamReader(request.Body).ReadToEndAsync();
var xSignature = request.Headers["X-Signature"].FirstOrDefault() ?? string.Empty;
var signingSecret = "your-signing-secret-here";

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingSecret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
var expected = Convert.ToHexString(hash).ToLowerInvariant();

if (expected != xSignature.ToLowerInvariant())
    return Results.Unauthorized();

var payload = JsonSerializer.Deserialize<JsonElement>(rawBody);
```

{% endtab %}

{% tab title="PHP" %}

```php
$rawBody       = file_get_contents('php://input');
$xSignature    = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$signingSecret = 'your-signing-secret-here';

$expected = hash_hmac('sha256', $rawBody, $signingSecret);

if ($expected !== $xSignature) {
    http_response_code(401);
    exit('Invalid signature');
}

$payload = json_decode($rawBody, true);
```

{% endtab %}

{% tab title="Ruby" %}

```ruby
require "openssl"

raw_body       = request.raw_post
x_signature    = request.headers["X-Signature"]
signing_secret = "your-signing-secret-here"

expected = OpenSSL::HMAC.hexdigest("SHA256", signing_secret, raw_body)

raise "Invalid signature" unless expected == x_signature

payload = JSON.parse(raw_body)
```

{% endtab %}
{% endtabs %}

***

## Security recommendations

| Recommendation                         | Detail                                                                                                                                                                                                                         |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Use a timing-safe comparison           | Always compare signatures using a constant-time function (e.g. `hmac.compare_digest`, `CryptographicOperations.FixedTimeEquals`, `hash_equals`). Standard string equality leaks timing information that attackers can exploit. |
| Read the raw body                      | Compute the HMAC over the **raw** request bytes, before any JSON parsing.                                                                                                                                                      |
| Store secrets in environment variables | Never hardcode your `signing_secret` in source code. Use environment variables or a dedicated secrets manager.                                                                                                                 |
| Rotate secrets if compromised          | If you suspect a secret has been exposed, delete the webhook subscription and create a new one to obtain a fresh secret.                                                                                                       |
| Respond promptly                       | Return a `2xx` response as quickly as possible — ideally before doing any heavy processing. Plannr may retry deliveries that do not receive a timely response.                                                                 |


# Add-On Store

There are two types of add-ons within Plannr's ecosystem; plannr-built add-ons and externally built. If you've built your own integration to Plannr, it's still possible to have an entry on our add-on store.&#x20;

### Key information required for a store entry

In order to shout about your integration within Plannr, we'll need the following information from you:&#x20;

* Name of your integration (max 255 chars)
* Headline banner image (1024 x 300 pixels)
* Description of what the add-on does (no limit on length, can include html)
* Instructions on how to set up the add-on (no limit on length, can include html)

### Listing your integration

To get your integration listed on our add-on store, please visit the following link. You are also welcome to send us any HTML you would like in the info/setup instructions by sending us an email to **<integrations@plannrcrm.com>**

{% embed url="<https://plannrcrm.com/add-on-form>" %}


# Launch Button

We also offer the ability to add a launch button to Plannr. This allows your integration to be launched fully from within the CRM. Your button would look like this:

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FV3So4epeGAv3cGNRAx9r%2FScreenshot%202024-08-19%20at%2014.18.21.png?alt=media&amp;token=3baa1e7a-0611-4ffb-be44-1c5bf444f360" alt=""><figcaption></figcaption></figure>

We allow launch buttons on the following pages:

* Client Page
* Plan Page

The purpose of launch buttons is to seamlessly open your integration with just a click of a button. While this can be just a link to your application, you can also complete the full OAuth2 flow when this button is pressed.&#x20;

### Example OAuth2 Flow

1. User clicks on your launch button
2. The button will redirect to a URL of your choosing with the account (client) UUID / plan UUID in the URL
3. You store the UUID in the browser session and redirect to <https://api.plannrcrm.com/oauth/authorize>
4. The user is redirected back to Plannr where the OAuth2 flow happens, and then Plannr will redirect back to your application
5. You check your session if it has a launch UUID present, if so, take them to the resource directly on your system.

### Adding launch buttons to Plannr

If you would like to add a launch button to Plannr, please let us know the URL you would like us to point a button to, as well as the text you would like in the button. Note, you must also have an add-on for launch buttons to be visible.

To add a launch button, email us at **<integrations@plannrcrm.com>**&#x20;


# Embeddable Integrations

Learn about how you can build embeddable integrations in Plannr, blurring the lines between your product and Plannr.

Embeddable integrations allow users to smoothly blend their products with Plannr's interface, eliminating the hassle of forcing users to switch between different products.

By building an embeddable integration you get to build your product in one place, and then distribute it to multiple solutions without the need to rebuild. You also get to retain control of releasing your own updates and improvements, too.

<figure><img src="https://970086564-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F64M46VyTVjy4JXpxfk6s%2Fuploads%2FK8cFUJcmLrBUveFeOtBj%2FCleanShot%202025-06-10%20at%2015.18.02.png?alt=media&amp;token=7399f241-6b98-4001-b9f9-008325683ab3" alt=""><figcaption><p>An example of an embedded integration (Be-IQ) in Plannr.</p></figcaption></figure>

### Where can you embed?

You can build an embeddable integration for the adviser portal, or the client portal, or both! There are various pages in both the client and adviser-facing portals where an integrator might want their product to appear but to give you some examples, here are some pages that might appeal to you:&#x20;

* The main dashboard of a contact (client) in the adviser-facing portal
* On a new page, in the client portal
* On the main adviser dashboard for Plannr
* At the top of the page when viewing a client record.&#x20;

You can either build in-page elements (or widgets) to appear on part of a user's screen or we can build you a page where your product appears as the primary content.

### How do we add our widget / integration?

Right now, we'll need to work with you to add your code to Plannr through one of our engineering team so you're best popping an email to <integrations@plannrcrm.com> to get this process kicked off. We will create you an ephemeral environment separate from any current Plannr instance you might have so we can test the unreleased embeddable widget code.

### How does the integration work?

In short: Plannr signs a short-lived JWT identifying the current user and the resource they are viewing, and hands it to your widget. You verify that JWT yourself, using a public key Plannr gives you up front. Nothing in this flow is a credential exchange with Plannr.

Two things it is deliberately **not**:

* It is **not a login or a session**. It does not sign anyone in to your service.
* It is **not used to call the Plannr API**. The token is never sent back to Plannr. It is for your widget to verify locally and nothing else.

#### What Plannr provides you

When your integration is set up, Plannr gives you two things:

1. **An addon code.** A short, opaque, 6-character code that uniquely identifies your addon. It appears as the `aud` (audience) claim in every token issued for your widget, and as a prefix on key identifiers (see Key rotation below). Use it to confirm a token was issued for your integration and not some other addon.
2. **A public key** (RS256) for verifying token signatures. You can either use the key handed to you directly, or fetch it from Plannr's JWKS endpoint (recommended, see Key rotation).

#### The embed token (JWT)

Each token is an RS256-signed JWT with a **30-minute expiry**. Its claims are:

| Claim                      | Meaning                                                                       |
| -------------------------- | ----------------------------------------------------------------------------- |
| `sub`                      | The current user viewing the widget                                           |
| `current_account_uuid`     | The user's current account                                                    |
| `resource_type`            | The type of resource being viewed (currently only `contact`)                  |
| `resource_id`              | The UUID of the resource being viewed                                         |
| `aud`                      | Your addon code. Confirm this matches the addon the token is being used with. |
| `iss`, `iat`, `exp`, `jti` | Standard issuer, issued-at, expiry, and token-ID claims                       |

The token header carries a **`kid`** (key ID) identifying which signing key produced the signature. You use this to select the correct public key when verifying (see Key rotation).

A token is only issued once Plannr has run a set of server-side access checks, and those checks are **re-evaluated on every token request**, not just when the firm first subscribed. From your side this has one practical consequence: a token can stop being issued at any time (for example if the firm's subscription ends or access is revoked). When that happens you will simply stop receiving a fresh token, which the refresh contract below handles as a pause/hide.

#### Receiving the token: the mount contract

Plannr passes the JWT to your widget via a **data attribute on the `div` your widget mounts into**. This is the only delivery mechanism Plannr supports directly. You can then use JavaScript to read this `div` and mount a VueJS / React component within (recommended), or an iframe.

Plannr does **not** provide a `postMessage` / iframe bridge. If your widget runs inside an iframe, you are responsible for the wrapper code that reads the data attribute off Plannr's `div` and relays it into your iframe, however you need to.

#### Verifying the token

Verification happens entirely in your widget, with no call back to Plannr:

1. Read the JWT from the data attribute on the mount `div`.
2. Read the `kid` from the token header and select the matching public key (fetched from the JWKS endpoint, or the key you were given).
3. Verify the RS256 signature against that public key.
4. Confirm `aud` matches your addon code.
5. Confirm the token has not expired (`exp`).

Only after all of these pass should you treat the `sub`, `current_account_uuid`, `resource_type`, and `resource_id` claims as a trustworthy statement of who is viewing what.

#### The refresh contract (what your widget must handle)

The token is short-lived, so Plannr refreshes it in the background while the page is open. **Your widget must handle the attribute changing after mount, not just read it once.**

Plannr is not going to remount your widget on each refresh, so your embedded code needs to watch the parent `div` for attribute changes (for example with a `MutationObserver`) and react live.

How Plannr manages the attribute:

* **Proactive refresh.** Plannr attempts to fetch a new token roughly 30 seconds before the current one expires, and repopulates the data attribute with it. The existing token stays valid right up until its TTL elapses, even if a refresh attempt is still in flight or is being retried with backoff.
* **Attribute removed means pause.** If Plannr cannot obtain a fresh token before the old one expires, it removes the data attribute, shows an error, hides the integration, and prompts the user to refresh the page. Your widget is **hidden, not unmounted or destroyed**. Treat the attribute being removed as "pause / hide."
* **Attribute reappears means resume.** If a fresh token arrives while the page is still open, Plannr repopulates the data attribute, clears the error, and reshows the integration. Treat the attribute reappearing (or changing to a new value) as "resume" and re-verify the new token.

So your minimum behaviour is: verify on mount, observe the attribute, re-verify on every change, and hide/pause whenever the attribute is absent.

#### Key rotation

Plannr can rotate its signing keys without invalidating tokens that are already in flight. This is why you match keys by `kid` rather than assuming a single fixed key.

Plannr publishes its public keys at a standard **JWKS endpoint**:

```
GET /.well-known/jwks.json
```

This exposes the signing keys currently valid for addon-issued JWTs, in standard JWKS format, so you can fetch and cache the public keys you need to verify tokens without any shared secret.

Two details worth knowing:

* **Key IDs are namespaced by addon.** Each key's `kid` is prefixed with the addon code it belongs to, in the form `<addon-code>-<ulid>`. This lets you (and Plannr) tell at a glance which addon a key or token belongs to, and keeps key IDs collision-free and non-sequential across integrators.
* **There is a grace period on rotation.** When Plannr activates a new key, the previous key is not deleted immediately; it moves to a "retiring" state and keeps verifying for a grace period so that tokens handed out just before the rollover continue to work. The JWKS endpoint exposes both the active and the retiring keys during this window.

The practical guidance: fetch the JWKS, cache it, and select the verifying key by the token's `kid`. Because keys rotate and a new `kid` can appear at any time, do not hardcode a single key. Refresh your cached JWKS when you encounter a `kid` you do not recognise.

#### Communicating with the Plannr API

As mentioned above, the JWT flow does not provide API access to Plannr so if your integration likely also needs to retrieve data from Plannr there are two ways you can do this:

* **Using OAuth2:** We offer OAuth2 access to all our integrators who want to build integrations for other firms on Plannr. You can set up this OAuth2 flow separately and then use the `jti`  claim on the JWT which gives you the user's UUID and the `current_account_uuid` gives you the account UUID of the person viewing the integration. You can use these to look up your existing tokens for this user.
* **Using personal access tokens:** We also offer personal access tokens for simpler integrations, say if you're a developer working for a firm. We recommend the firm creates an additional user within Plannr, logs into that user and creates a personal access token from the settings area. You can then use this PAT to read data.

{% hint style="info" %}
Never expose access tokens or personal access tokens directly to the web. You should always store these securely server-side and use a proxy layer to send data back and forth. We recommend using the JWT we provide you in every HTTP request so you can verify it has always come from the integration and is verified with your public key.
{% endhint %}

#### Implementation checklist

* [ ] Read the JWT from the data attribute on the `div` Plannr mounts your widget into.
* [ ] If running in an iframe, provide your own wrapper that relays the attribute value into the iframe.
* [ ] Fetch and cache Plannr's public keys from the JWKS endpoint; select the key by the token's `kid`, refreshing the cache on an unknown `kid`.
* [ ] Verify the RS256 signature, that `aud` matches your addon code, and that the token has not expired.
* [ ] Watch the mount `div` for attribute changes (for example `MutationObserver`) and re-verify on every change.
* [ ] Treat the attribute being removed as pause/hide, and its reappearance as resume.
* [ ] Keep your own Plannr API authentication separate; this embed token is never sent back to Plannr.


