SimplyPut Apps API
Introduction
The SimplyPut Apps API enables developers to create applications that interact with the spaces in your SimplyPut account. This API allows for querying data, receiving various response types, and customizing user experiences based on organizational data.
Getting Started
Before you begin, ensure you have the necessary prerequisites:
- A SimplyPut account
- Spaces setup with connected data sources
- A web-accessible server for your app’s webhook
Creating and Configuring Your App
To create and configure your app, follow these steps:
- Create a new app on https://app.simplyput.ai/org/[your_org_id]/settings/apps
- Configure your app settings, including the webhook URL
- Retrieve your app’s publishable key
Understanding the Sequence
There are four main contacts points to understand when interacting with the SimplyPut Apps API:
- 1️⃣ Client sends a POST request to the API
- 2️⃣ Webhook receives a POST request from the API containing the question and metadata
- 3️⃣ Webhook replies with a go/no-go response code, metadata, and additional instructions, usually after querying your own application database for user permissions and data scope
- 4️⃣ API returns a response containing the AI-generated message and accompanying data
Sequence Diagram
- Your client (as implemented in your applicaiton environment) initiates the interaction by sending a POST request to the SimplyPutAPI. This request contains a question along with some metadata.
- Once the request reaches the SimplyPutAPI, it performs initial validation to check if the provided publishable key (unique identifier for an app) is correct and whether the request format adheres to the expected structure.
- If the validation is successful, the SimplyPutAPI then forwards the request, along with the metadata, to a specific part called the Webhook.
- The Webhook takes over to figure out who the user is by analyzing the provided metadata. It then checks with the OrgDatabase to gather information on what the user is allowed to do (user permissions) and what specific data they can access (data scope).
- The OrgDatabase responds to the Webhook by providing details like the user’s role, position, and other relevant context.
- With this information, the Webhook makes a decision and sends back a modified version of the original request along with an approval status, indicating whether the API request is allowed
- If the request is approved, the SimplyPutAPI then queries the SimplyPutSpaces to retrieve the data based on the question and metadata.
- The SimplyPutSpaces then queries the OrgDatabase in real-time to retrieve the data.
- Finally, the SimplyPutAPI returns the response to the client, which contains the data or a message.
1️⃣ Making API Requests
To interact with the SimplyPut Apps API, send POST requests to the endpoint:
- https://rest.simplyput.ai/app/v1/ask
Include your question and any relevant metadata in the request body.
curl --request POST \
--url https://rest.simplyput.ai/app/v1/ask \
--header 'Content-Type: application/json' \
--header 'x-app-key: <your-publishable-key>' \
--data '{
"question": "<your-question>",
"metadata": {
"auth_jwt": "<auth-token-for-your-webhook-to-identify-user>"
}
}'
Replace <your-app-key> with your app’s publishable key, and <your-question> with the query you wish to ask. Metadata values are optional. They can be used to provide additional context to your webhook, and/or to populate any corresponding ${variables} in your app’s instructions
Field | Requirement | Description | Data Type |
---|---|---|---|
x-app-key | Required | Your app’s publishable key | String |
question | Required | The question that was asked by the user | String |
threadId | Optional | SimplyPut’s unique identifier for the thread. Can be used to provide additional context to your webhook to provide the most appropriate response | String |
metadata | Optional | Values that can be used to provide additional context to your webhook, and/or to populate any corresponding ${variables} in your app’s instructions | Object |
2️⃣ Webhooks
Before processing a question, the SimplyPut Apps API sends a webhook request to your server. This allows you to modify the request, add additional context, and control the response based on user details.
Generally speaking, the server hosting your webhook will have access to your main application database. This will enable the webhook to do any of the following:
- Resolve the provided client token to a user id and full name
- Verify that the user has access to the requested data
- Block the request if the user is not authorized
- Limit the scope of the data spaces the user can access
- Override or censor the request based on user roles or sensitive keywords
- Log the question to your own application database
- Add additional context to the request
Webhook—Incoming Payload
When a question is sent to your SimplyPut /app/v1/ask endpoint, a request is sent to your server before processing the question. Here’s an example of the request your server will receive:
{
"questionId": "<unique-question-identifier>",
"threadId": "<unique-thread-identifier>",
"question": "How many non-fiction paperback books were sold in Q3 at the New York store?",
"metadata": {
"auth_jwt": "<auth-token-for-your-webhook-to-identify-user>"
}
}
Field | Description | Data Type |
---|---|---|
questionId | SimplyPut’s unique identifier for the question. Can be used to track the question in your own application database | String |
threadId | SimplyPut’s unique identifier for the thread. Can be used to provide additional context to your webhook to provide the most appropriate response | String |
question | The question that was asked by the user | String |
metadata | Values that were provided in the metadata field of the original request | Object |
3️⃣ Webhook—Expected Response
Your webhook can modify the request or add additional metadata. Here’s an example response from your webhook that SimplyPut will use to process the question:
🟢 Success Response (HTTP 200 Status Code){
"metadata": {
"userId": "user-identifier",
"userEmail": "user@example.com"
},
"additionalInstructions": "Assume that the home office is in New York, the user’s role is ’store manager', and their employee id is '12345'",
"allowedSpaces": ["space-identifier-for-allowed-data"],
"disallowedSpaces": ["space-identifier-for-disallowed-data"]
}
Field | Requirement | Description | Data Type |
---|---|---|---|
HTTP Status Code | Required | Must be set to 200 in order to process the question, otherwise the request will be aborted | HTTP Status Code |
metadata | Optional | Values that can be used to provide additional context to your webhook, and/or to populate any corresponding ${variables} in your app’s instructions | Object |
additionalInstructions | Optional | Additional instructions to be added to the original request | String |
allowedSpaces | Optional | Array of space identifiers that the user is allowed to access. Space ids may be found and copies from the app settings. This may may not be used in conjunction with disallowedSpaces. If neither allowedSpaces nor disallowedSpaces are provided, the user will be allowed to access all spaces | Array |
disallowedSpaces | Optional | Array of space identifiers that the user is not allowed to access. Space ids may be found and copies from the app settings. This may may not be used in conjunction with allowedSpaces. If neither allowedSpaces nor disallowedSpaces are provided, the user will be allowed to access all spaces | Array |
If the webhook response includes a non-200 status code, the request will be aborted and the payload will be passed through as-is into the response to the client. The response schema should conform to IETF RFC 9457 (Problem Details for HTTP APIs). Here’s an example of an error response:
{
"type": "https://example.com/docs/error",
"title": "Unauthorized",
"status": 401,
"detail": "The user is not authorized to access the requested data"
// ... additional fields as needed
}
Field | Requirement | Description | Data Type |
---|---|---|---|
HTTP Status Code | Required | Typically set to 4xx or 5xx to indicate the error | HTTP Status Code |
type | Required | A URI reference that identifies the problem type | String |
title | Required | A short, human-readable summary of the problem | String |
status | Optional | The HTTP status code, mirroring the status code of the response | Number |
detail | Optional | A human-readable explanation specific to this occurrence of the problem | String |
text | Optional | A mirror of the detail field. This field is included to simplify populating the data that will be handled by the bot associated with the app | String |
Example: Webhook Payload Processing and Response
Here’s an example of how you can process the webhook payload and respond to it using Express.js:
//webhook server function
const app = express();
app.use(bodyParser.json());
app.post('/webhook', async function (req, res) {
res.setHeader('Content-Type', 'application/json');
// Retrieve the data from the request
const { url, query, headers, body } = req;
const { question, metadata: { auth_token } } = body;
//Query your own database to retrieve these values
// based on the provided auth_token
const { name,
username,
home_office,
role,
employee_id,
lang } = await lookupUser(auth_token);
// Check if the user is an employee on file
if (!employee_id) {
const errorMessage = `I don’t recognize you. Please contact HR.`;
res.status(401).json({
type: 'https://example.com/docs/error',
title: 'Unknown Employees Not Allowed',
status: 401,
detail: errorMessage,
text: errorMessage
return;
}
// Screen the question based your own criteria
const disallowedQuestion = await checkForSensitiveContent(question);
if(disallowedQuestion) {
const errorMessage = `Sorry, ${name}, but I can’t let you ask that.`;
res.status(406).json({
type: 'https://example.com/docs/error',
title: 'Question Contains Sensitive Content',
status: 406,
detail: errorMessage,
text: errorMessage
return;
}
// Populate the payload to return in the response
const data = {
// These values will used to:
// 1. Replace variables in the app instructions
// 2. Set session variables in the database with a prefix of 'sp.'
metadata: { name, username, home_office, role, employee_id },
};
//Determine if any spaces should be disallowed or explicitly allowed
const isCorporate = /corporate/i.test(home_office);
const isHR = /hr/i.test(role);
const isSales = /sales/i.test(role);
if (isSales) {
// Only give access to the sales space
data.allowedSpaces = ['jmf2nzUYyMzUvp3t'];
} else if (!isCorporate && !isHR) {
// Only corporate and HR can see salary data
data.disallowedSpaces = ['mY3ky49pjEGHVAET'];
}
// Note: Space ids may be found and copied from the app page
// Optionally add any additional context or instructions
if(lang==='es') {
data.additionalInstructions = `Por favor responda en español.`;
}
// Optionally override the question
data.question = `${question}, please and thank you. `;
// Respond
res.status(200).json(data);
});
// Start the server
app.listen(80, () => {
console.log('Webhook server is running on http://<hostname>:80/webhook');
});
In this example, the webhook server receives a question and metadata from the SimplyPut Apps API. It then follows these steps to process the request and prepare a suitable response payload for SimplyPut to receive and process:
- Retrieves the user’s details from the database based on the provided auth token
- Checks if the user is valid, responds with an error if not
- Checks if the question contains sensitive content, responds with an error if it does
- Prepares the metadata object to be sent back to the SimplyPut Apps API
- Optionally sets the allowedSpaces and disallowedSpaces arrays to limit the user’s access to specific data spaces based on their role and home_office
- Optionally adds additional instructions based on the user’s settings
- Optionally overrides the question
- Responds with a 200 status code and the payload
Variables in the App Instructions
App instructions are set on the app page. These can be used to prompt the AI to respond in certain ways or may be used to populate the response with additional context. Sometimes general instructions will still require request-specific context. In these cases, the instructions may include a variable placeholder. For example:
- Assume that the home office is ${home_office}, the user’s role is ${role}, and their employee id is ${employee_id}
- Limit any time-based queries to the last ${time_period}
- When querying financial data, assume that the currency is ${currency}
The variables defined in the app instructions will be replaced with the corresponding values from the metadata object in the webhook response. For example, if the metadata object contains the key home_office with the value New York, ${home_office} in the app instructions will be replaced with New York in the response.
🗄️ Row-level Security (Optional)
While identifying the user via string replacement and AI-generated WHERE clauses is often an adequate solution, there are use cases in which a more secure approach is required to avoid the risk of accidental data leaks. Row-level security is a feature available in many databases that allows you to restrict access to rows in a table by creating policies that define the criteria for accessing the rows. This ensures that only the right users can access the right rows without relying on prompts or instructions to the AI.
Implementing row level security is achieved by following these steps:
- Enable row-level security on the tables you want to protect
- Create a policy that defines the criteria for accessing the rows
- Reference session variables in the policy to enforce the criteria.
- Session variables are set by SimplyPut and are based on the metadata object sent in the webhook response and/or the API request
- Each session variable set by SimplyPut is prefixed with sp.
Example: Row-level Security in PostgreSQL
Here’s an example of how you can use row-level security in PostgreSQL to limit access to rows in a table based on the user’s role or other criteria.
- Note: This step only needs to be done once for each table you want to protect.
-- Enable Row Level Security on the tables that you want to protect
ALTER TABLE employee_salary ENABLE ROW LEVEL SECURITY;
ALTER TABLE sales_data ENABLE ROW LEVEL SECURITY;
ALTER TABLE incident_reports ENABLE ROW LEVEL SECURITY;
-- Create a policy so employees can only see their own salary
CREATE POLICY salary_access_policy ON employee_salary
TO service_account_used_by_simplyput
USING (current_setting('sp.user_employee_id')::INT = employee_id);
-- Create a policy so store managers can only see data from their store
CREATE POLICY sales_access_policy ON sales_data
TO service_account_used_by_simplyput
USING (current_setting('sp.user_home_office')::TEXT = store_location);
-- Create a policy so only HR can see incident reports
CREATE POLICY hr_incident_access_policy ON incident_reports
TO service_account_used_by_simplyput
USING (current_setting('sp.user_role')::TEXT = 'hr_manager');
In this example, three tables are protected using row-level security. The employee_salary table is protected so that only the employee whose ID matches the session variable sp.user_employee_id can see their salary. The sales_data table is protected so that only the store manager whose home office matches the session variable sp.user_home_office can see the data. The incident_reports table is protected so that only HR managers whose role matches the session variable sp.user_role can see the data. These policies only apply to the DB user used by SimplyPut ( service_account_used_by_simplyput) so setting these policies will not impact other users of the database.
⚙️ Setting Session Variables
Every time a SimplyPut app runs a query, each value in the metadata object is set as a session variable in the database. The session variable is named after each item’s key, and its value is set to the corresponding value in the metadata object.
Example: Session Variables in PostgreSQL
Prior to running its query, SimplyPut will run the following variable setting command(s), one for each metadata value sent in the request. In this example, values for user_id, user_role, user_home_office, and user_employee_id are retrieved from the webhook metadata and are set as session variables in the database before the query is run:
- Note: These commands are injected automatically by SimplyPut at runtime; no action is required beyond passing the metadata in the request and/or the webhook response.
SET sp.user_employee_id = '12345';
SET sp.user_role = 'store_manager';
SET sp.user_home_office = 'New York';
In this example, if an employee in the New York office asks “Show me the sales numbers for January,” the query will trigger the sales_access_policy and automatically query the equivalent of a hardcoded WHERE without having to put these details in the prompt or instructions:
SELECT * FROM sales_data WHERE store_location = 'New York' AND month = 'January';
You can now ensure that no user will ever see data that goes beyond their permissions, even if the AI generates a query that would otherwise return it.
Note: Session variables are cleared after each query, so you don't need to worry about them persisting between requests.
4️⃣ Processing the API Response
When you query the SimplyPut Apps API, it returns a structured JSON response containing various fields that provide insight into the asked question, the processing status, and relevant data or instructions. Understanding these fields is crucial for effectively processing and displaying the results or feedback to your users. Below is a detailed overview of the typical fields you might encounter in an API response.
🟢 Success Response (HTTP 200 Status Code){
"questionId": "unique-question-identifier",
"threadId": "unique-thread-identifier",
"space": "Sales and Revenue Data",
"sql": "SELECT * FROM sales_data WHERE store_location = 'New York'",
"results": {
"columns": [...],
"rows": [...],
}
"text": "Here are the sales numbers for New York.",
"metadata": {
// echoed metadata from the webhook response and/or the API request
},
"content": "Detailed content or explanation related to the query",
"explanation": "Human-readable explanation of the query processing"
}
Field | Description | Data Type |
---|---|---|
questionId | A unique identifier for the question asked. | String |
threadId | A unique identifier for the thread that the question was asked in. For new threads, this value should be saved and passed in as the threadId in the follow-up questions to maintain context. | String |
space | Descriptive name of the data space that the answer was obtained from. | String |
text | Human-readable response. Generally this is the string that would be spoken by a bot. | String |
results | Data returned in response to the query, often as an array or object. | columns[] & rows[] |
sql | The SQL query representation that was generated from the question. | String |
metadata | Echoes the metadata provided to the API by the webhook response and/or the API request. | Object |
This structured response allows for sophisticated query processing and response handling on your frontend. By leveraging these fields, you can implement a dynamic, conversational interface that can not only present the answer in a user-friendly manner but also utilize the returned data for further contextual interactions or visualizations.
🔴 Error ResponseErrors may occur either from a non-200 webhook response or from an error generated by the SimplyPut Apps API. In either case, the response schema wiill conform to IETF RFC 9457 (Problem Details for HTTP APIs). Here’s an example of an error response:
{
"type": "https://example.com/docs/error",
"title": "Unauthorized",
"status": 401,
"detail": "The user is not authorized to access the requested data"
"text": "Sorry, but you are not authorized to access the requested data"
}
If the text field is present in the response, the UI can process it and display the error message to the user with th existing error handling logic. If not, the UI implementation will need to parse the response and determine how if or how to display the error message to the user.
🌐 Example: Frontend Integration
Here is a minimal ReactJS example of how you can integrate the SimplyPut Apps API into a React component to allow users to ask questions and display the response. This example uses the axios library to send the API request and manage the response.
Overview
The component has a text input for the user to ask a question and a button to send the question to the API. When the user clicks the button, the fetchAnswer function is called. This function sends a POST request to the SimplyPut Apps API with the question and any other necessary metadata. The API response is destructured to extract the text and results fields, which are then displayed in the UI.
import React, { useState } from 'react';
import axios from 'axios';
const SimplyPutComponent = () => {
const [question, setQuestion] = useState('');
const [text, setText] = useState('');
const [results, setResults] = useState({});
const fetchAnswer = async () => {
try {
const url = `https://rest.simplyput.ai/app/v1/ask`;
const data = {
question: question,
metadata: {
auth_jwt: '<auth-token-to-securely-identify-user>'
}
};
const publishableKey = `<your-publishable-key>`; // Find this on https://app.simplyput.ai/org/[your_org_id]/settings/apps
const apiResponse = await axios.post(url, data, {
headers: {
'Content-Type': 'application/json',
'x-app-key': publishableKey
}
});
const {text, results: {columns, rows}, sql, threadId} = apiResponse.data;
// See the 'Processing the API Response' for details on what to expect in the response
setText(text);
setResults({columns, rows});
//clear question from UI box
setQuestion('');
} catch (error) {
console.error('API call error:', error);
}
};
return (
<div>
<input
type="text"
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="Ask a question..."
/>
<button onClick={fetchAnswer}>Ask</button>
{response?.text && (
<div>
<h2>Response:</h1>
<pre>{response.text || "No Response"}</pre>
</div>
)}
{
response?.results && (
<div>
<h2>Results:</h1>
<pre>{JSON.stringify(response.results, null, 2)}</pre>
</div>
)
}
</div>
);
};
export default SimplyPutComponent;
Glossary
Here are some terms that are commonly used in the context of the SimplyPut Apps API:
- App: A SimplyPut app is a collection of settings and configurations that define how the SimplyPut Apps API should process questions and respond to them.
- Data space: A data space is a collection of data that is accessible to the app. Data spaces are defined in the app settings and are used to limit the scope of the data that the user can access.
- Metadata: Values that can be used to provide additional context to your webhook, and/or to populate any corresponding ${variables} in your app’s instructions.
- Publishable key: A key that is used to authenticate your app when making requests to the SimplyPut Apps API. This key is found on the app page.
- Webhook: A webhook is a mechanism that allows you to modify the request, add additional context, and control the response based on user details before processing a question.
- Webhook payload: The data that is sent to your server when a question is asked. This data includes the question, the metadata, and other details about the question.
- Row-level security: A feature available in many databases that allows you to restrict access to rows in a table by creating policies that define the criteria for accessing the rows.
- Session variables: Variables that are set by SimplyPut and are based on the metadata object sent in the webhook response and/or the API request. Each session variable set by SimplyPut is prefixed with sp.
- Thread: A thread is a sequence of questions and responses that are related to a specific topic or context. The threadId is used to maintain context between questions. http response
- API Status Code: A status code that is returned by the SimplyPut Apps API to indicate the success or failure of a request. A status code of 200 indicates success, while any other status code indicates an error.