Managing OAuth Connections
Use your API key and public key to initiate, list, inspect, and call Salesforce on behalf of your users.
Overview
Every Salesforce account that your user connects through Appnigma Integrations is stored as a Connection — an encrypted record linking a user's Salesforce org to one of your integrations. This guide shows you how to manage those connections end-to-end using your credentials.
You get two credentials when you create an integration:
API Key
Server-side only. Used to list connections, read their credentials, and make proxied Salesforce API calls. Never expose this in a browser.
Public Key
Frontend-safe. Used only to initiate the OAuth flow from a browser. Cannot read data or call APIs on its own.
All examples below use the official
@appnigma/integrations-client
Node SDK and the appnigma-integrations-client
Python SDK.
Install the SDK
npm install @appnigma/integrations-clientyarn add @appnigma/integrations-clientpnpm add @appnigma/integrations-clientpip install appnigma-integrations-clientInitialize the client once on your backend:
import { AppnigmaClient } from '@appnigma/integrations-client';
const appnigma = new AppnigmaClient({
apiKey: process.env.APPNIGMA_API_KEY,
baseUrl: 'https://integrations.appnigma.ai', // optional
});from appnigma_integrations_client import AppnigmaClient
appnigma = AppnigmaClient(
api_key=os.environ["APPNIGMA_API_KEY"],
base_url="https://integrations.appnigma.ai", # optional
)The Python SDK is async — all methods must be called with await inside
an async function. Use async with AppnigmaClient(...) as client: for
automatic session cleanup.
1. Initiate a connection (public key)
This is the only step that uses your public key, and it happens in the browser. Redirect your user to the connect URL and AP-Integrations handles the entire OAuth handshake with Salesforce.
Connect URL format
https://integrations.appnigma.ai/connect/{integrationId}?public_key={publicKey}
The ID of the integration you created via the admin API.
Your frontend-safe public key. Safe to include in client-side code.
Tip
A redirect_uri query parameter is also supported but not required.
If omitted, the user will see a default success page after connecting.
Example: backend route that builds the URL
app.get('/api/connect-url', (req, res) => {
const url =
`https://integrations.appnigma.ai/connect/${process.env.AP_INTEGRATION_ID}` +
`?public_key=${encodeURIComponent(process.env.AP_PUBLIC_KEY)}` +
`&redirect_uri=${encodeURIComponent('https://yourapp.com/connected')}`;
res.json({ url });
});from urllib.parse import quote
@app.route("/api/connect-url")
def connect_url():
url = (
f"https://integrations.appnigma.ai/connect/{os.environ['AP_INTEGRATION_ID']}"
f"?public_key={quote(os.environ['AP_PUBLIC_KEY'])}"
f"&redirect_uri={quote('https://yourapp.com/connected')}"
)
return jsonify({"url": url})Example: frontend button
<button id="connect">Connect Salesforce</button>
<script>
document.getElementById('connect').onclick = async () => {
const { url } = await fetch('/api/connect-url').then(r => r.json());
location.href = url;
};
</script>After the user approves, they land on your redirect_uri with a
connection_id you should persist against your internal user record.
2. List connections (API key)
Fetch every connection tied to your integration. Useful for building an admin dashboard or letting users pick which org to query.
const result = await appnigma.listConnections({
status: 'connected',
environment: 'production',
limit: 20,
});
console.log(result.connections);
// [
// {
// connectionId: 'abc-123',
// userEmail: 'sarah@acme.com',
// userName: 'Sarah Chen',
// orgName: 'Acme Corp',
// status: 'connected',
// ...
// }
// ]async with AppnigmaClient(api_key=os.environ["APPNIGMA_API_KEY"]) as client:
result = await client.list_connections(
status="connected",
environment="production",
limit=20,
)
print(result["connections"])
# [
# {
# 'connectionId': 'abc-123',
# 'userEmail': 'sarah@acme.com',
# 'userName': 'Sarah Chen',
# 'orgName': 'Acme Corp',
# 'status': 'connected',
# ...
# }
# ]Query parameters
Filter by status: connected, expired, revoked, deleted.
production or sandbox.
Case-insensitive substring match against userEmail.
Max connections to return per page.
Pagination cursor from the previous response's nextCursor.
Response
Array of connection summaries. Encrypted tokens are never included here.
Number of connections in this page.
Opaque cursor for the next page. Omitted when there are no more results.
3. Inspect a single connection's credentials (API key)
Retrieve a live, decrypted access token for a specific connection. The token is refreshed automatically if it's within 5 minutes of expiring.
Returning a raw access token over the wire is powerful. Only call this endpoint server-side, and never forward the token to a browser.
const creds = await appnigma.getConnectionCredentials('conn-abc-123');
console.log(creds);
// {
// accessToken: '00D...xxxx',
// instanceUrl: 'https://acme.my.salesforce.com',
// environment: 'production',
// region: 'NA',
// tokenType: 'Bearer',
// expiresAt: '2026-04-10T14:32:00.000Z'
// }async with AppnigmaClient(api_key=os.environ["APPNIGMA_API_KEY"]) as client:
creds = await client.get_connection_credentials("conn-abc-123")
print(creds)
# {
# 'accessToken': '00D...xxxx',
# 'instanceUrl': 'https://acme.my.salesforce.com',
# 'environment': 'production',
# 'region': 'NA',
# 'tokenType': 'Bearer',
# 'expiresAt': '2026-04-10T14:32:00.000Z'
# }When to use this
- You need to call a Salesforce endpoint that isn't supported by the proxy
- You're embedding a Salesforce SDK that expects a raw access token
- You're building a background worker that holds a token for a short burst of operations
For most use cases, prefer the proxy (next section) — it handles auth, refresh, and usage tracking for you.
4. Make proxied Salesforce API calls (API key)
The proxy is the easiest way to talk to Salesforce. You never see the token — you just describe the request you want to make.
const result = await appnigma.proxySalesforceRequest('conn-abc-123', {
method: 'GET',
path: '/services/data/v59.0/query',
query: {
q: 'SELECT Id, Name FROM Account LIMIT 10',
},
});
console.log(result.records);async with AppnigmaClient(api_key=os.environ["APPNIGMA_API_KEY"]) as client:
result = await client.proxy_salesforce_request(
"conn-abc-123",
{
"method": "GET",
"path": "/services/data/v59.0/query",
"query": {"q": "SELECT Id, Name FROM Account LIMIT 10"},
},
)
print(result["records"])Request body
HTTP method: GET, POST, PUT, PATCH, or DELETE.
Salesforce API path, e.g. /services/data/v59.0/query. Do not include
the instance URL.
Query string parameters (merged into the final URL).
JSON body for POST/PUT/PATCH requests.
What the proxy does for you
- Validates your API key and looks up the connection
- Refreshes the token automatically if it's close to expiring
- Forwards your request to the correct Salesforce instance URL
- Tracks the call against your plan's usage quota
- Returns the raw Salesforce response
Example: create an Opportunity
const newOpp = await appnigma.proxySalesforceRequest('conn-abc-123', {
method: 'POST',
path: '/services/data/v59.0/sobjects/Opportunity',
data: {
Name: 'Acme Deal Q2',
StageName: 'Prospecting',
CloseDate: '2026-06-30',
Amount: 50000,
},
});
console.log(newOpp.id); // '006xx0000011AbCdEF'async with AppnigmaClient(api_key=os.environ["APPNIGMA_API_KEY"]) as client:
new_opp = await client.proxy_salesforce_request(
"conn-abc-123",
{
"method": "POST",
"path": "/services/data/v59.0/sobjects/Opportunity",
"data": {
"Name": "Acme Deal Q2",
"StageName": "Prospecting",
"CloseDate": "2026-06-30",
"Amount": 50000,
},
},
)
print(new_opp["id"]) # '006xx0000011AbCdEF'End-to-end example
Here's a complete minimal backend that implements the user journey: user connects Salesforce → your app queries their pipeline → user can disconnect.
import express from 'express';
import 'dotenv/config';
import { AppnigmaClient } from '@appnigma/integrations-client';
const app = express();
const INTEGRATIONS_BASE = "https://integrations.appnigma.ai";
const appnigma = new AppnigmaClient({ apiKey: process.env.APPNIGMA_API_KEY });
// Step 1: browser asks for the connect URL
app.get('/api/connect-url', (_req, res) => {
const url =
`${INTEGRATIONS_BASE}/connect/${process.env.AP_INTEGRATION_ID}` +
`?public_key=${process.env.AP_PUBLIC_KEY}` +
`&redirect_uri=${encodeURIComponent('https://yourapp.com/connected')}`;
res.json({ url });
});
// Step 2: AP-Integrations redirects here after approval
app.get('/connected', (req, res) => {
const { connection_id } = req.query;
// Persist connection_id against your internal user
res.redirect(`/dashboard?connection_id=${connection_id}`);
});
// Step 3: list all connected orgs
app.get('/api/orgs', async (_req, res) => {
const result = await appnigma.listConnections({ status: 'connected' });
res.json(result.connections);
});
// Step 4: query a specific connection's pipeline
app.get('/api/pipeline/:connectionId', async (req, res) => {
const result = await appnigma.proxySalesforceRequest(req.params.connectionId, {
method: 'GET',
path: '/services/data/v59.0/query',
query: {
q: `SELECT Id, Name, Amount, StageName FROM Opportunity
WHERE IsClosed = false ORDER BY Amount DESC LIMIT 25`,
},
});
res.json(result.records);
});
app.listen(4000);import os
import asyncio
from flask import Flask, request, jsonify, redirect
from urllib.parse import quote
from appnigma_integrations_client import AppnigmaClient
app = Flask(__name__)
def run_async(coro_fn):
"""Bridge async SDK calls into sync Flask routes."""
async def _run():
async with AppnigmaClient(api_key=os.environ["APPNIGMA_API_KEY"]) as client:
return await coro_fn(client)
return asyncio.run(_run())
# Step 1: browser asks for the connect URL
@app.route("/api/connect-url")
def connect_url():
url = (
f"https://integrations.appnigma.ai/connect/{os.environ['AP_INTEGRATION_ID']}"
f"?public_key={quote(os.environ['AP_PUBLIC_KEY'])}"
f"&redirect_uri={quote('https://yourapp.com/connected')}"
)
return jsonify({"url": url})
# Step 2: AP-Integrations redirects here after approval
@app.route("/connected")
def connected():
connection_id = request.args.get("connection_id")
# Persist connection_id against your internal user
return redirect(f"/dashboard?connection_id={connection_id}")
# Step 3: list all connected orgs
@app.route("/api/orgs")
def list_orgs():
result = run_async(lambda c: c.list_connections(status="connected"))
return jsonify(result["connections"])
# Step 4: query a specific connection's pipeline
@app.route("/api/pipeline/<connection_id>")
def pipeline(connection_id):
result = run_async(
lambda c: c.proxy_salesforce_request(
connection_id,
{
"method": "GET",
"path": "/services/data/v59.0/query",
"query": {
"q": "SELECT Id, Name, Amount, StageName FROM Opportunity "
"WHERE IsClosed = false ORDER BY Amount DESC LIMIT 25",
},
},
)
)
return jsonify(result["records"])
app.run(port=4000)Error handling
All SDK methods throw errors with a status property matching the HTTP
status code returned by AP-Integrations.
try {
const result = await appnigma.proxySalesforceRequest(connectionId, { ... });
} catch (err) {
if (err.status === 404) {
// Connection not found or deleted
} else if (err.status === 401) {
// Connection expired — user needs to reconnect
} else if (err.status === 429) {
// Monthly request limit exceeded
} else {
// Unknown error
console.error(err);
}
}from appnigma_integrations_client import AppnigmaAPIError
try:
async with AppnigmaClient(api_key=api_key) as client:
result = await client.proxy_salesforce_request(connection_id, {...})
except AppnigmaAPIError as err:
if err.status_code == 404:
# Connection not found or deleted
pass
elif err.status_code == 401:
# Connection expired — user needs to reconnect
pass
elif err.status_code == 429:
# Monthly request limit exceeded
details = err.get_details()
print(f"Usage: {details['currentUsage']}/{details['planLimit']}")
else:
# Unknown error
print(f"{err.error}: {err.message}")Common error codes
| Status | Meaning | What to do |
|---|---|---|
400 | Missing required field | Check your request body |
401 | Invalid API key or expired connection | Prompt user to reconnect |
403 | API key doesn't match the integration | Verify key/integration pairing |
404 | Connection not found | Prompt user to connect |
429 | Plan limit exceeded | Upgrade plan or wait for next month |