One API.
SOC + Help Desk.
Fully headless.
Sam is the AI platform that powers Pink Duck — a 24/7 security operations center and autonomous help desk. Use these docs to plug Sam in anywhere.
https://9vy0x0o0cc.execute-api.us-east-2.amazonaws.com
Create your account at sam.pinkduckcompany.com — dashboard only, no API needed.
Exchange credentials for a Cognito ID token and pass it as a Bearer on every request.
Every endpoint is live immediately. UI and API share the same data. Multi-tenancy is baked in.
Authenticate with the API
Sam uses AWS Cognito for authentication. Exchange your credentials for an ID token, then pass it as a Bearer value in the Authorization header on every request. Tokens expire after one hour.
async function exchange(userPoolId, clientId, email, password) {
const region = userPoolId.split('_')[0];
const resp = await fetch(`https://cognito-idp.${region}.amazonaws.com/`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-amz-json-1.1',
'X-Amz-Target': 'AWSCognitoIdentityProviderService.InitiateAuth',
},
body: JSON.stringify({
AuthFlow: 'USER_PASSWORD_AUTH',
ClientId: clientId,
AuthParameters: { USERNAME: email, PASSWORD: password },
}),
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.message);
return data.AuthenticationResult.IdToken;
}
const token = await exchange(
'us-east-2_J5UAtrmqZ', // User Pool ID
'l988ndjcmbcfim10t25qehh1p', // App Client ID
'you@yourcompany.com',
'your-password'
);
// Attach to every request
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
};
List tickets
Returns tickets in your account. Filter by status or date. For MSP-wide queries across all client accounts, use /msp/ticket instead.
| Code | Label | Description |
|---|---|---|
| O | Open | Actively being worked. Default help desk queue view. |
| OE | Scheduled | Scheduled for a later date. |
| OB | Blocked | Waiting on the customer before work can continue. |
| OR | Needs review | Sam's analysis requires human sign-off. |
| C | Resolved | Confirmed resolved and archived. |
| CI | No action | Nothing actionable to complete. |
| CR | Accepted risk | Customer accepted the risk on this ticket. |
| CS | Auto-closed | Sam closed with high confidence. |
const BASE = 'https://9vy0x0o0cc.execute-api.us-east-2.amazonaws.com';
// Open tickets in current account
const open = await fetch(`${BASE}/ticket?status=O`, { headers })
.then(r => r.json());
// Tickets updated in a given month
const month = await fetch(`${BASE}/ticket?date=2026-01`, { headers })
.then(r => r.json());
// MSP-wide: open across all client accounts
const mspOpen = await fetch(`${BASE}/msp/ticket?status=O`, { headers })
.then(r => r.json());
Get a ticket
Fetch a single ticket by ID, including its full update history. Tickets carry a service tag: 1 Security, 2 Help Desk, 3 Business Risk, 4 Project.
const ticket = await fetch(`${BASE}/ticket/TKT-00147`, { headers })
.then(r => r.json());
// ticket.touches — threaded updates in order
// ticket.status — single character status code
// ticket.service — 1=Security 2=HelpDesk 3=BizRisk 4=Project
Create a ticket
Creating a ticket is two steps. First POST /ticket to create the record, then POST /ticket/{id} with the returned ID to add an opening touch — a threaded message, like the first entry in a conversation thread.
// Step 1: Create the ticket
const ticket = await fetch(`${BASE}/ticket`, {
method: 'POST',
headers,
body: JSON.stringify({
name: 'Printer offline — east wing 3rd floor',
source: 'api', // 'google' | 'microsoft' | 'api'
service: 2, // 1=Security 2=HelpDesk 3=BizRisk 4=Project
}),
}).then(r => r.json());
// Step 2: Add the opening touch
await fetch(`${BASE}/ticket/${ticket.id}`, {
method: 'POST',
headers,
body: JSON.stringify({
value: 'HP LaserJet not responding. Tried power cycling — no change.',
user: 'jane@acmecorp.com',
}),
}).then(r => r.json());
Update a ticket
Patch any field on a ticket — most commonly status to move it through the workflow. Only fields you include are changed.
X for auditability.from into into, consolidating touches.// Close a ticket
await fetch(`${BASE}/ticket/TKT-00147`, {
method: 'PATCH',
headers,
body: JSON.stringify({ status: 'C' }),
}).then(r => r.json());
// Change due date
await fetch(`${BASE}/ticket/TKT-00147`, {
method: 'PATCH',
headers,
body: JSON.stringify({ due: '2026-11-15'}),
}).then(r => r.json());
// Merge duplicate tickets
await fetch(`${BASE}/ticket/TKT-00147/merge/TKT-00151`, {
method: 'POST', headers,
}); // TKT-00151 merged into TKT-00147
List monitors
Monitors are integrations Sam connects to on your behalf. Once connected, Sam continuously scans for compliance issues and security incidents.
const monitors = await fetch(`${BASE}/integration`, { headers })
.then(r => r.json());
// [ { name: 'microsoft' }, { name: 'google' }, ... ]
Connect a monitor
Connect a new monitor by POSTing credentials to /integration/{name}. The body is the credential object for that service. Sam validates and begins scanning immediately on success.
See the full list of available integrations in the Monitors section of the Sam interface.
// Google Workspace — service account JSON key
await fetch(`${BASE}/integration/google`, {
method: 'POST',
headers,
body: JSON.stringify({
type: 'service_account',
project_id: 'your-project-id',
private_key_id: '...',
private_key: '-----BEGIN RSA PRIVATE KEY-----\n...',
client_email: 'sam@your-project.iam.gserviceaccount.com',
}),
}).then(r => r.json());
// Microsoft 365 — app registration credentials
await fetch(`${BASE}/integration/microsoft`, {
method: 'POST',
headers,
body: JSON.stringify({
tenant_id: 'your-tenant-id',
client_id: 'your-app-client-id',
client_secret: 'your-app-secret',
}),
}).then(r => r.json());
Disconnect a monitor
Send DELETE to /integration/{key}. Sam immediately stops scanning and removes stored credentials. Existing tickets created by the monitor are preserved.
await fetch(`${BASE}/integration/google`, {
method: 'DELETE',
headers,
});
Account delegation
By default every API call operates in your root MSP account. To act on behalf of a client, pass the account header set to their domain. Sam scopes all reads and writes to that account for any request carrying the header.
No separate login, token exchange, or session — just the header. Your root credentials authorize you for every account your MSP manages.
account header works on every endpoint — tickets, integrations, team, files, and audit. Drop it to fall back to your MSP root context.// Act on behalf of a client account
const clientHeaders = { ...headers, 'account': 'acmecorp.com' };
// Open tickets in the client account
const tickets = await fetch(`${BASE}/ticket?status=O`, { headers: clientHeaders })
.then(r => r.json());
// Loop all clients, print open ticket counts
const accounts = await fetch(`${BASE}/msp/account`, { headers })
.then(r => r.json());
for (const acct of accounts) {
const open = await fetch(`${BASE}/ticket?status=O`, {
headers: { ...headers, account: acct.domain },
}).then(r => r.json());
console.log(acct.domain, open.length, 'open');
}
Invite a customer
Invite a customer to your managed services. They receive an email to accept, after which you can delegate into their account.
// Send invitation
await fetch(`${BASE}/msp/invitation`, {
method: 'POST',
headers,
body: JSON.stringify({ to: 'customer@acmecorp.com' }),
});
// Assign a team member to manage the account
await fetch(`${BASE}/msp/account/acmecorp.com/engineer@pinkduckcompany.com`, {
method: 'POST', headers,
});
// Remove them from the account
await fetch(`${BASE}/msp/account/acmecorp.com/engineer@pinkduckcompany.com`, {
method: 'DELETE', headers,
});
Send an email and Sam will create your first help desk ticket.