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.

9 min read · Updated 28 September 2026

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:

ProviderFamilyHow it connectsConnections
mysql, mariadb, postgres, sqlserverdatabasecredentialsunlimited
smtp, postmark, resend, sendgridemailcredentialsone email connection in total per owner, whichever provider
pipedrivecrmcredentials1
activecampaigncrmcredentials1
brevocrmcredentials1
google_drivestorageOAuth in a browser1, 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 poll GET /integration/connections?provider=google_drive until 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

StatusExample bodyMeaning
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

GET /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

GET /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

POST /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.

Common body fields
name string required
A name people will recognise, for example “Northwind Retail DB”.
description string optional
Optional.
scope string required
personal or workspace. workspace needs a workspace owner or admin.
database_permission string optional default read_only
Databases only: read_only, create_update or full_access. See permission levels.
Database fields (mysql, mariadb, postgres, sqlserver)
host string required
Host name as seen from StickyPrompts, or from the jump host when you use SSH.
port integer optional
Default 3306 (MySQL, MariaDB), 5432 (PostgreSQL), 1433 (SQL Server).
database string required
schema string optional
PostgreSQL default public, SQL Server default dbo.
username string required
Use a dedicated user with the least privilege the job needs.
password string required
ssl_mode string 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_host string optional
Route through this SSH jump host.
ssh_port integer optional
ssh_username string optional
Required with ssh_host.
Email fields (smtp, postmark, resend, sendgrid)
default_from_email string 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_name string optional
default_reply_to string optional
server_token string optional
Postmark.
api_key string optional
Resend and SendGrid.
host, port, username, password, encryption various optional
SMTP. encryption is starttls (default), ssl or none; port defaults to 587, or 465 with ssl.
CRM fields
api_token string optional
Pipedrive (required).
api_url, api_key string optional
ActiveCampaign (both required; api_url is your account URL).
api_key string optional
Brevo (required).
allow_write boolean optional default false
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

GET /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

PATCH /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

DELETE /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

POST /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:

LevelAllowed
read_onlyExactly one SELECT or WITH statement.
create_updateExactly one SELECT, WITH, INSERT or UPDATE statement.
full_accessAnything 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

GET /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

GET /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

POST /api/v1/integration/connections/:connectionId/database/query
Body
query string required
One SQL statement.
row_limit integer optional default 100
Maximum 1000.
timeout_seconds integer optional default 10
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.

Email

Mail always goes out through the connected account’s own credentials; there is no StickyPrompts-provided sender.

GET /integration/connections/:connectionId/email/connection

GET /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

POST /api/v1/integration/connections/:connectionId/email/send
Body
to Address[] required
At least one {"email": "...", "name": "..."}.
cc, bcc Address[] optional
to, cc and bcc together: at most 50 recipients.
reply_to Address[] optional
Defaults to the connection’s reply-to.
subject string required
html_body string optional
Send html_body, text_body or both.
text_body string optional
from_email, from_name string optional
Override the sender for this message. Your provider may refuse an unverified sender.
attachment_file_ids integer[] 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

GET /api/v1/integration/connections/:connectionId/crm/connection

{"provider": "pipedrive", "allow_write": true} (plus api_url for ActiveCampaign).

GET /integration/connections/:connectionId/crm/contacts

GET /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

GET /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

POST /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

PATCH /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

GET /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

GET /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

POST /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

GET /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

GET /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

GET /api/v1/integration/:provider/storage/search

query (required) plus the same paging and filters.

GET /integration/:provider/storage/entry/:entryId

GET /api/v1/integration/:provider/storage/entry/:entryId

POST /integration/:provider/storage/import

POST /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

POST /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.