Integrations
Create and manage connections to databases, email senders, CRMs and Google Drive, then query, send, search and write through them - the same connections chat, agents and workflows use.
Integrations let StickyPrompts act on your own systems. Through the API you can create connections, then use them directly: run SQL against a database, send an email, search or update a CRM, and move files to and from Google Drive. Everything here is the same machinery the app uses when someone asks for it in chat.
All paths are relative to https://api.stickyprompts.com/api/v1. The key acts as its owner: the same sharing and admin rules apply as in the app.
Concepts
Providers
:provider in paths, and provider in responses, use these exact strings:
| Provider | Family | How it connects | Connections |
|---|---|---|---|
mysql, mariadb, postgres, sqlserver | database | credentials | unlimited |
smtp, postmark, resend, sendgrid | credentials | one email connection in total per owner, whichever provider | |
pipedrive | crm | credentials | 1 |
activecampaign | crm | credentials | 1 |
brevo | crm | credentials | 1 |
google_drive | storage | OAuth in a browser | 1, personal only |
Limits count per owner: per user for personal connections, per workspace for shared ones.
Scope
Every connection is personal (only its owner sees and uses it) or workspace (everyone in the workspace can use it). Only workspace owners and admins can create, change, delete or read the stored details of a workspace connection; any member can use one - query it, send through it, read and, with write access, write CRM records. A connection you cannot see answers 404, never 403.
Credentials versus OAuth
- Database, email and CRM connections are created in one call with credentials in the body. The server checks them against the provider first: it opens the database connection, authenticates to SMTP, or sends a test message through Postmark, Resend or SendGrid. A pure API client can do this end to end.
- Google Drive uses OAuth. The connect call returns an
auth_url; a person must open it and approve Google’s consent screen. Your code can hand the link to a user and pollGET /integration/connections?provider=google_driveuntil the connection appears.
Secrets are never returned by any endpoint.
Status
A connection’s status is active, needs_reauth, revoked or error. Any connection route called on a non-active connection answers 409 {"error":"reconnect_required"}. Today only Google Drive connections leave active; repair them with reconnect.
Errors
| Status | Example body | Meaning |
|---|---|---|
400 | "invalid request", "query is required" | Malformed input. |
403 | "you do not have permission to manage this connection" | Workspace connection and you are not an owner or admin. |
403 | "this connection is read-only; enable write access on the connection to create or edit records" | CRM write without write access. |
404 | "integration is not connected or does not exist" | Unknown connection, or not yours. |
409 | "reconnect_required", "connection limit reached for this integration" | Needs reauthorising, or over the limit. |
413 | "the attachments are too large to send" | |
422 | "failed to connect to the integration with the given credentials: ..." | Credentials rejected on connect or update. |
422 | "query is not a single read-only select statement" | SQL not allowed at the connection’s permission level. |
422 | "the message was rejected: ..." | The email provider refused the message. |
501 | "integration provider does not support this capability: ..." | For example deals on a CRM without deals. |
502 | "failed to send the message: ...", "failed to reach the crm provider: ..." | The upstream provider failed. |
504 | "query timed out" | The database query ran past its timeout. |
500 | "internal server error" | Unmapped errors, including SQL errors raised by the database itself. |
Catalogue and connections
GET /integration
/api/v1/integration The provider catalogue, with what each can do and how many connections you already have.
[
{
"provider": "postgres",
"display_name": "PostgreSQL",
"capabilities": ["test_connection", "schema_browse", "query"],
"availability": "available",
"connection_count": 1,
"max_connections": 0
}
]
max_connections: 0 means unlimited. The shared limit of one email connection is not reflected here.
GET /integration/ssh-tunnel/public-key
/api/v1/integration/ssh-tunnel/public-key Database connections can reach a private database through an SSH jump host. Add this public key to authorized_keys of the SSH user on your jump host, and allow StickyPrompts’ address through your firewall.
{ "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... stickyprompts" }
POST /integration/:provider/connect
/api/v1/integration/:provider/connect Creates a new connection (it never replaces one). The body is one flat object: the common fields plus the provider’s own.
-
namestring required - A name people will recognise, for example “Northwind Retail DB”.
-
descriptionstring optional - Optional.
-
scopestring required personalorworkspace.workspaceneeds a workspace owner or admin.-
database_permissionstring optional defaultread_only - Databases only:
read_only,create_updateorfull_access. See permission levels.
-
hoststring required - Host name as seen from StickyPrompts, or from the jump host when you use SSH.
-
portinteger optional - Default 3306 (MySQL, MariaDB), 5432 (PostgreSQL), 1433 (SQL Server).
-
databasestring required -
schemastring optional - PostgreSQL default
public, SQL Server defaultdbo. -
usernamestring required - Use a dedicated user with the least privilege the job needs.
-
passwordstring required -
ssl_modestring optional - PostgreSQL:
disable,require(default),verify-ca,verify-full. MySQL/MariaDB:true,false,skip-verify,preferred(default). SQL Server:true(default),skip-verify,false. -
ssh_hoststring optional - Route through this SSH jump host.
-
ssh_portinteger optional -
ssh_usernamestring optional - Required with
ssh_host.
-
default_from_emailstring required - The default sender. For Postmark, Resend and SendGrid it must already be a sender your provider accepts - the connection test sends a real test message.
-
default_from_namestring optional -
default_reply_tostring optional -
server_tokenstring optional - Postmark.
-
api_keystring optional - Resend and SendGrid.
-
host, port, username, password, encryptionvarious optional - SMTP.
encryptionisstarttls(default),sslornone;portdefaults to 587, or 465 withssl.
-
api_tokenstring optional - Pipedrive (required).
-
api_url, api_keystring optional - ActiveCampaign (both required;
api_urlis your account URL). -
api_keystring optional - Brevo (required).
-
allow_writeboolean optional defaultfalse - Without it the connection is read-only: adding or editing contacts and creating deals answer
403.
curl -X POST https://api.stickyprompts.com/api/v1/integration/postgres/connect \
-H "Authorization: Bearer $STICKY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Northwind Retail DB",
"scope": "workspace",
"database_permission": "read_only",
"host": "localhost",
"port": 5432,
"database": "retail",
"schema": "public",
"username": "sp_reader",
"password": "********",
"ssl_mode": "disable",
"ssh_host": "bastion.northwind.example",
"ssh_port": 22,
"ssh_username": "stickyprompts"
}'import os, requests
r = requests.post(
"https://api.stickyprompts.com/api/v1/integration/postmark/connect",
headers={"Authorization": f"Bearer {os.environ['STICKY_API_KEY']}"},
json={
"name": "Northwind Mailer",
"scope": "workspace",
"default_from_email": "team@northwind.example",
"default_from_name": "Northwind Team",
"server_token": os.environ["POSTMARK_SERVER_TOKEN"],
},
)
print(r.status_code, r.json()) # 200 {"connection_id": 42}const res = await fetch("https://api.stickyprompts.com/api/v1/integration/pipedrive/connect", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.STICKY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Pipedrive - Sales",
scope: "workspace",
api_token: process.env.PIPEDRIVE_TOKEN,
allow_write: true,
}),
});
console.log(res.status, await res.json()); // 200 {"connection_id": 43}Response 200 {"connection_id": 42}. For google_drive the response is 200 {"auth_url": "..."} instead - send a person to that URL to finish.
GET /integration/connections
/api/v1/integration/connections Every connection you can see (yours plus your workspace’s shared ones), newest first. Optional ?provider=<provider>.
[
{
"id": 42,
"provider": "postgres",
"name": "Northwind Retail DB",
"description": "Demo retail data: stores, orders, products, customers.",
"scope": "workspace",
"status": "active",
"account_name": "sp_reader@localhost:5432/retail",
"connected_at": "2026-09-28T12:55:00Z",
"database_permission": "read_only"
}
]
account_name is a non-secret label: user@host:port/database for databases, the default sender for email, the account URL for ActiveCampaign. CRM write access is not in this list; read it from GET .../crm/connection.
PATCH /integration/connections/:connectionId
/api/v1/integration/connections/:connectionId Rename, change the database permission level, or rotate credentials. Send name every time (the current one if you are not renaming). description is always written, so omitting it clears it. Only the provider fields you send change; if any credential field is present, the merged configuration is re-verified before saving (422 on failure). For databases, "ssh_host": "" turns the SSH tunnel off. Returns 200 with an empty body.
DELETE /integration/connections/:connectionId
/api/v1/integration/connections/:connectionId Deletes the connection and its stored credentials. For Google Drive the grant is revoked at Google first. Returns 204.
POST /integration/connections/:connectionId/reconnect
/api/v1/integration/connections/:connectionId/reconnect Starts a fresh OAuth consent for a Google Drive connection and keeps its ID and name. Returns 200 {"auth_url": "..."}. Credential providers answer 501; update those with PATCH.
Databases
Permission levels
The connection’s database_permission decides which SQL the query endpoint accepts, whatever the API key may do:
| Level | Allowed |
|---|---|
read_only | Exactly one SELECT or WITH statement. |
create_update | Exactly one SELECT, WITH, INSERT or UPDATE statement. |
full_access | Anything the database user may run. No statement filtering. |
For read_only and create_update the whole statement is scanned for forbidden keywords, so a read-only query that merely mentions a word like update in a column name or string is refused. For MySQL, MariaDB and PostgreSQL, read-only sessions are also opened in read-only mode; SQL Server relies on the keyword check alone. In every case, connect with a database user that only has the rights you intend to give.
GET /integration/connections/:connectionId/database/schema
/api/v1/integration/connections/:connectionId/database/schema Every table and view in the connection’s schema, with columns:
{
"schema": {
"name": "public",
"tables": [
{
"schema": "public",
"name": "demo_orders",
"columns": [
{ "name": "id", "data_type": "integer", "nullable": false },
{ "name": "customer_id", "data_type": "integer", "nullable": false },
{ "name": "status", "data_type": "text", "nullable": true }
]
}
]
}
}
GET /integration/connections/:connectionId/database/connection
/api/v1/integration/connections/:connectionId/database/connection The stored connection details without the password. For workspace connections, owners and admins only. Fields you left empty come back as 0 or "" even though a default was used to connect.
POST /integration/connections/:connectionId/database/query
/api/v1/integration/connections/:connectionId/database/query -
querystring required - One SQL statement.
-
row_limitinteger optional default100 - Maximum 1000.
-
timeout_secondsinteger optional default10 - Maximum 30.
curl -X POST https://api.stickyprompts.com/api/v1/integration/connections/42/database/query \
-H "Authorization: Bearer $STICKY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "SELECT c.name, SUM(o.total) AS revenue FROM demo_orders o JOIN demo_customers c ON c.id = o.customer_id GROUP BY c.name ORDER BY revenue DESC LIMIT 5"}'import os, requests
r = requests.post(
"https://api.stickyprompts.com/api/v1/integration/connections/42/database/query",
headers={"Authorization": f"Bearer {os.environ['STICKY_API_KEY']}"},
json={"query": "SELECT name, city FROM demo_customers ORDER BY name", "row_limit": 50},
)
data = r.json()
rows = [dict(zip(data["columns"], row)) for row in data["rows"]]const res = await fetch(
"https://api.stickyprompts.com/api/v1/integration/connections/42/database/query",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.STICKY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: "SELECT name, city FROM demo_customers ORDER BY name" }),
},
);
const { columns, rows, truncated } = await res.json();Response for a SELECT:
{
"columns": ["name", "revenue"],
"rows": [["Hawthorne Goods", "18240.00"], ["Cedar & Pine Outfitters", "15110.50"]],
"row_count": 2,
"truncated": false
}
truncated: true means more rows existed beyond row_limit. Numeric columns often arrive as strings. Other statements (allowed at higher levels) return only row_count, the rows affected. SQL errors raised by the database return 500 without the database’s message, so test queries in a database client first.
Mail always goes out through the connected account’s own credentials; there is no StickyPrompts-provided sender.
GET /integration/connections/:connectionId/email/connection
/api/v1/integration/connections/:connectionId/email/connection The sender settings (provider, default_from_email, default_from_name, default_reply_to, plus host, port, username and encryption for SMTP). Tokens and passwords are never returned. For workspace connections, owners and admins only.
POST /integration/connections/:connectionId/email/send
/api/v1/integration/connections/:connectionId/email/send -
toAddress[] required - At least one
{"email": "...", "name": "..."}. -
cc, bccAddress[] optional to,ccandbcctogether: at most 50 recipients.-
reply_toAddress[] optional - Defaults to the connection’s reply-to.
-
subjectstring required -
html_bodystring optional - Send
html_body,text_bodyor both. -
text_bodystring optional -
from_email, from_namestring optional - Override the sender for this message. Your provider may refuse an unverified sender.
-
attachment_file_idsinteger[] optional - Up to 10 StickyPrompts files you own. Size limits after encoding: SMTP 25 MB, Postmark 10 MB, SendGrid 30 MB, Resend 40 MB.
curl -X POST https://api.stickyprompts.com/api/v1/integration/connections/41/email/send \
-H "Authorization: Bearer $STICKY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": [{ "email": "sales@northwind.example", "name": "Sales team" }],
"subject": "Pipedrive: Deals at Proposal Made",
"html_body": "<p>2 deals at Proposal Made, worth $28,788.</p>",
"text_body": "2 deals at Proposal Made, worth $28,788."
}'
Response 200 {"message_id": "<...>", "provider": "postmark"} - the provider’s own ID, handy for finding the message in its dashboard.
CRM
Reads work on every CRM connection. Writes - adding or editing contacts and creating deals - need allow_write on the connection.
Contact: email (required on every write, and the lookup key for edits), first_name, last_name, phone, company, custom_fields (provider-specific), list_ids (Brevo, ActiveCampaign) and tag_ids (ActiveCampaign). Results come back as {"remote_id", "provider", "contact"}.
Deal: title, value, currency, contact_email (must belong to an existing contact) and stage (a free-form label). Results come back as {"remote_id", "provider", "deal"}.
GET /integration/connections/:connectionId/crm/connection
/api/v1/integration/connections/:connectionId/crm/connection {"provider": "pipedrive", "allow_write": true} (plus api_url for ActiveCampaign).
GET /integration/connections/:connectionId/crm/contacts
/api/v1/integration/connections/:connectionId/crm/contacts Search contacts with query (required) and optional provider filters in bracket notation, for example ?query=priya&filters[organization_id]=1. Pipedrive and ActiveCampaign search free text; Brevo looks up one exact email or ID.
GET /integration/connections/:connectionId/crm/contacts/list
/api/v1/integration/connections/:connectionId/crm/contacts/list Browse contacts. limit (default 50), then page with cursor (Pipedrive: pass the previous next_cursor) or offset (ActiveCampaign, Brevo). Returns {"contacts": [...], "has_more": true, "next_cursor": "..."}.
POST /integration/connections/:connectionId/crm/contacts
/api/v1/integration/connections/:connectionId/crm/contacts Adds a contact. It always creates a new one, even if the email exists - search first.
{ "email": "priya.raman@hawthornegoods.example", "first_name": "Priya", "last_name": "Raman", "company": "Hawthorne Goods" }
PATCH /integration/connections/:connectionId/crm/contacts
/api/v1/integration/connections/:connectionId/crm/contacts Edits the contact with that email; only the fields you send change (send "" to clear one). Never creates: 404 "no matching contact was found in the crm to update".
GET /integration/connections/:connectionId/crm/deals
/api/v1/integration/connections/:connectionId/crm/deals Search deals by title or the linked contact’s email with query, plus provider filters (for Pipedrive: status, person_id, organization_id, exact_match).
GET /integration/connections/:connectionId/crm/deals/list
/api/v1/integration/connections/:connectionId/crm/deals/list Browse deals, paged like contacts. Pipedrive filters include stage_id, status, owner_id and pipeline_id.
POST /integration/connections/:connectionId/crm/deals
/api/v1/integration/connections/:connectionId/crm/deals { "title": "Cedar & Pine - Scale plan, 42 stores", "value": 4788, "currency": "USD", "contact_email": "marcus.webb@cedarpine.example", "stage": "Proposal Made" }
404 if no contact has that email.
GET /integration/connections/:connectionId/crm/lists, /crm/tags
/api/v1/integration/connections/:connectionId/crm/lists Lists (Brevo, ActiveCampaign) and tags (ActiveCampaign) as [{"id": "12", "name": "Newsletter"}], for use in filters and in a contact’s list_ids / tag_ids. Other providers answer 501.
Google Drive
The storage routes always act on your own personal Drive connection and take the provider in the path: /integration/google_drive/storage/.... Folder and entry IDs are root or Drive IDs.
GET /integration/:provider/storage/entries
/api/v1/integration/:provider/storage/entries List a folder: folder_id (default root), page_token, page_size (1-200, default 100), kind (folder or file). Returns {"entries": [...], "next_page_token": "", "breadcrumbs": [...]}. Each entry has id, name, kind, mime_type, size, modified_at, web_url, importable and import_name (Docs and Drawings import as PDF, Sheets as XLSX, Slides as PPTX).
GET /integration/:provider/storage/search
/api/v1/integration/:provider/storage/search query (required) plus the same paging and filters.
GET /integration/:provider/storage/entry/:entryId
/api/v1/integration/:provider/storage/entry/:entryId POST /integration/:provider/storage/import
/api/v1/integration/:provider/storage/import Copy 1-20 Drive entries (entry_ids) into StickyPrompts as regular files, optionally attached to a project_id. Each entry succeeds or fails on its own, so always check failed: {"files": [...], "failed": [{"entry_id": "...", "error": "..."}]}. Up to 100 MB per file.
POST /integration/:provider/storage/export
/api/v1/integration/:provider/storage/export Copy a StickyPrompts file you own (file_id) into a Drive folder (folder_id, root allowed), optionally renamed with name.