API Reference
REST API for Pinkk. Submit intelligence runs, read analyses and reports, manage profiles, checks, comparisons, workspaces, and webhook endpoints. All responses are JSON.
https://trypinkk.com/api/v1
/api/v1/
Authentication
Every request must include a personal access token as a bearer header. Tokens are created and managed at /api-access. The full token is shown once at creation — store it securely. API access is gated behind a subscriber plan.
Authorization: BearerAccept: application/json
Tokens are scoped to your account — the API only returns data belonging to the account that created the token. Revoke a token at any time from the dashboard.
Team accounts: the API uses the team owner's data and credit wallet regardless of which member created the token.
Scopes
When creating a token you select the scopes it may use. A request to an endpoint outside the token's scopes returns 403. An expired token returns 401.
read:user
Read account info, plan, credits, and limits.
read:profiles
List and read profiles.
write:profiles
Create and update profiles.
read:analyses
List and read analyses, reports, and PDFs.
write:analyses
Submit new analyses (spends credits).
read:genes
Read available analysis types.
read:checks
List and read checks.
write:checks
Submit new checks.
read:comparisons
List and read comparisons.
write:comparisons
Create comparisons.
read:workspaces
List and read workspaces.
write:workspaces
Create, update, manage workspace membership.
read:webhooks
List and read webhook endpoints.
write:webhooks
Register, update, and delete webhook endpoints.
Errors
Standard HTTP status codes. All error bodies contain at minimum a message string. Validation errors (422) also include an errors object keyed by field name.
400
Bad request — e.g. Idempotency-Key exceeds 255 characters.
401
Unauthenticated — missing, malformed, revoked, or expired token.
402
Insufficient credits — cannot submit, top up your wallet first.
403
Forbidden — plan doesn't include API access, or token lacks the required scope. Body includes "upgrade": true when a plan upgrade is needed.
404
Not found — resource doesn't exist or doesn't belong to your account. 404 is returned instead of 403 to prevent probing for other accounts' IDs.
409
Conflict — Idempotency-Key was reused with a different request body.
422
Unprocessable — invalid input. Check the errors object.
429
Too many requests — rate limit exceeded. Back off and retry.
503
Service unavailable — a required sub-system is temporarily offline.
{
"message": "The selected status is invalid.",
"errors": {
"status": ["The selected status is invalid."]
}
}
Pagination
All list endpoints return a paginated data array with links and meta. Follow links.next until it is null, or use meta.last_page to calculate total pages.
page
Page number. Defaults to 1.
per_page
Results per page, 1–100. Defaults to 20.
{
"data": [ /* items */ ],
"links": {
"first": "https://trypinkk.com/api/v1/analyses?page=1",
"last": "https://trypinkk.com/api/v1/analyses?page=5",
"prev": null,
"next": "https://trypinkk.com/api/v1/analyses?page=2"
},
"meta": {
"current_page": 1,
"last_page": 5,
"per_page": 20,
"total": 94
}
}
Idempotency
POST endpoints that create resources accept an optional Idempotency-Key: <string ≤255 chars> header. Retry the same key + body and you get the original response replayed — no second credit charge, no duplicate resource. Keys are scoped per user (not per token) and expire after 24 hours. 422 and 429 responses are not cached, so you can fix the request and retry under the same key.
Supported on: POST /analyses, POST /profiles, POST /checks, POST /comparisons, POST /workspaces.
Rate limits
60 req / min per user
additional per-route throttle
additional per-route throttle
When rate limited the response is 429 Too Many Requests. Back off exponentially and retry.
Endpoints
Account
/user
read:user
Returns the account associated with the token — plan, credit balance, remaining analysis slots, and feature limits.
{
"data": {
"id": 1,
"name": "Acme Agency",
"email": "[email protected]",
"plan": { "key": "agency", "name": "Agency", "is_subscriber": true },
"credits": { "balance": 42 },
"limits": {
"can_submit_analysis": true,
"remaining_analyses": 10,
"analyses_this_period": 40,
"max_analyses_per_period": 50
}
}
}
Profiles
A profile represents a creator, brand, or affiliate being researched. Analyses are run against profiles.
/profiles
read:profiles
List your profiles, newest first.
type
string
Filter by profile type: creator, brand, or affiliate.
per_page
integer
Results per page, 1–100. Default: 20.
/profiles
write:profiles
Create a new profile. Supports Idempotency-Key.
type
string
required
creator, brand, or affiliate.
profile_label
string
required
Display name for this profile.
platforms
array
required
Array of { platform, value } objects. e.g. [{"platform":"instagram","value":"@handle"}].
vertical_pack
string
optional
Intelligence lens. Defaults to generic.
internal_note
string
optional
Private note, max 500 chars.
{
"data": {
"id": "01KS7BDHFPV10MP72CJE07Y0VN",
"label": "Spookyloopz",
"type": "creator",
"vertical_pack": "generic",
"platforms": [
{ "platform": "instagram", "handle": "spookyloopz", "url": "https://instagram.com/spookyloopz" }
],
"created_at": "2026-05-22T13:00:00+00:00"
}
}
/profiles/{id}
read:profiles
Single profile with platforms, fetched metrics, and intelligence grade.
/profiles/{id}
write:profiles
Update profile fields. Accepts the same fields as POST; all are optional.
Analyses
An analysis is a completed intelligence run against a profile using a specific gene (analysis type). Each costs one credit.
/analyses
read:analyses
List analyses, newest first. The list omits the heavy report.content body — fetch the single-item endpoint to get full content.
status
string
Filter: pending, processing, completed, failed.
gene
string
Filter by gene key, e.g. traffic_gene.
profile_id
string
Filter by profile ULID.
per_page
integer
Results per page, 1–100. Default: 20.
/analyses
write:analyses
Submit a new analysis. Deducts one credit from your balance. If an identical analysis is already in-flight, returns 200 with meta.in_flight: true and no credit is charged. Supports Idempotency-Key.
profile_id
string
required
ULID of the profile to analyse.
gene_key
string
required
Analysis type key from GET /genes.
vertical_pack
string
optional
Intelligence lens. Defaults to the profile's lens, then generic.
notes
string
optional
Analyst note, max 500 chars (only if the gene supports notes).
{
"data": {
"id": "01KS8AZMF4E2XN3G7JKWRDHQ01",
"status": "processing",
"gene": { "key": "traffic_gene", "label": "Traffic Gene" },
"profile": { "id": "01KS7BDHFPV10MP72CJE07Y0VN", "label": "Spookyloopz" },
"report": null,
"created_at": "2026-05-22T13:01:04+00:00",
"started_at": null,
"completed_at": null
},
"meta": { "credits_charged": 1 }
}
Poll GET /analyses/{id} until status is completed or failed, or register a webhook to avoid polling.
/analyses/{id}
read:analyses
Single analysis with full report.content. Content shape varies by gene — treat unknown keys defensively.
{
"data": {
"id": "01KS7VWT2B6W8G6D2BKAGDNJYW",
"status": "completed",
"gene": { "key": "traffic_gene", "label": "Traffic Gene" },
"profile": { "id": "01KS7BDHFPV10MP72CJE07Y0VN", "label": "Spookyloopz" },
"report": {
"reference_number": "PKK-WEEV-2PBX",
"title": "Traffic Gene: Spookyloopz",
"schema_version": "1.0",
"public_summary": "Audience looks credible, with search-driven traffic.",
"content": { /* gene-specific sections */ }
},
"created_at": "2026-05-22T12:52:19+00:00",
"completed_at": "2026-05-22T12:52:28+00:00"
}
}
/analyses/{id}/report
read:analyses
The report object only — same shape as in the single-analysis response, without surrounding metadata. Returns 404 until the report exists. Useful for polling.
/analyses/{id}/pdf
read:analyses
Download the report as a PDF. Returns binary application/pdf. Returns 404 if the report is not yet complete.
Genes
A gene is an analysis type — it defines what signals are researched and what report shape is produced.
/genes
read:genes
List available analysis types. Use the key field as gene_key when submitting an analysis. available_to_you reflects your current plan.
{
"data": [
{
"key": "traffic_gene",
"label": "Traffic Gene",
"description": "Assesses traffic source patterns and audience credibility.",
"credit_cost": 1,
"available_to_you": true
}
]
}
Checks
A check is a free-text evidence lookup — no profile required. Paste a domain, social handle, brand name, or suspicious offer and get a cited, confidence-rated assessment. Processing is async; poll until status is completed. No webhook events exist for checks.
/checks
read:checks
List checks. Accepts ?status= and ?per_page=.
/checks
write:checks
Submit a check. If the same subject was researched recently and a cached result is available, returns it immediately at no credit cost. Supports Idempotency-Key.
subject
string
required
Free-text subject — domain, handle, brand name, etc.
force_live
boolean
optional
Set true to skip the cache and always run fresh research.
{
"data": {
"id": "01KS8BZMF4E2XN3G7JKWRDHQ02",
"status": "processing",
"subject": "example.com",
"content": null
},
"meta": { "resolution": "live", "credits_charged": 1 }
}
A cached hit returns 200 with meta.resolution: "cache" and credits_charged: 0.
/checks/{id}
read:checks
Single check. When status is completed, the content field contains the cited assessment.
Comparisons
A comparison pits two completed analyses of the same gene against each other, producing a verdict and section-by-section signal breakdown.
/comparisons
read:comparisons
List comparisons.
/comparisons
write:comparisons
Create a comparison. Both analyses must be completed and use the same gene. Returns 422 if the genes differ ("Both reports must come from the same analysis type to be compared."). Supports Idempotency-Key.
analysis_a_id
string
required
ULID of the first completed analysis.
analysis_b_id
string
required
ULID of the second completed analysis (same gene).
notes
string
optional
Analyst question or context, max 500 chars.
/comparisons/{id}
read:comparisons
Single comparison with verdict and content.
Workspaces
Workspaces group profiles into named collections. A profile can belong to multiple workspaces.
/workspaces
read:workspaces
List workspaces.
/workspaces
write:workspaces
Create a workspace. Body: name (required), description (optional), profile_ids[] (optional). Supports Idempotency-Key.
/workspaces/{id}
read:workspaces
Single workspace with member profiles.
/workspaces/{id}
write:workspaces
Update name or description.
/workspaces/{id}
write:workspaces
Delete workspace (profiles are not deleted).
/workspaces/{id}/profiles
write:workspaces
Attach profiles. Body: { profile_ids: [ulid, …] }.
/workspaces/{id}/profiles/{profile}
write:workspaces
Detach a profile from the workspace.
Webhooks
Register HTTPS endpoints to receive callbacks when analyses complete or fail. Manage endpoints via the API or from your account dashboard.
/webhooks
read:webhooks
List webhook endpoints.
/webhooks/{id}
read:webhooks
Single webhook endpoint.
/webhooks/{id}
write:webhooks
Delete a webhook endpoint.
/webhooks
write:webhooks
Register a new webhook endpoint. HTTPS only. The signing secret is returned once in the response — store it immediately.
url
string
required
HTTPS URL to deliver events to.
events
array
required
Events to subscribe to: ["analysis.completed", "analysis.failed"].
description
string
optional
Label for this endpoint, max 255 chars.
{
"data": {
"id": "01KS8CZMF4E2XN3G7JKWRDHQ03",
"url": "https://example.com/pinkk-hook",
"events": ["analysis.completed", "analysis.failed"],
"enabled": true,
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
// secret is shown only in this response — store it immediately.
/webhooks/{id}
write:webhooks
Enable or disable an endpoint, update events, or change the description. Send only the fields you want to change.
enabled
boolean
optional
true to enable, false to disable.
events
array
optional
Replace the subscribed events list.
description
string
optional
Update the endpoint label.
Webhook events
analysis.completed
Fired after the report is written. Payload includes the full analysis with report.content.
analysis.failed
Fired only on permanent failure — after all retries are exhausted. Not fired on intermediate retry attempts.
{
"id": "evt_01KS8DZMF4E2XN3G7JKWRDHQ04",
"type": "analysis.completed",
"created_at": "2026-05-22T13:05:00+00:00",
"data": {
"analysis": { /* full AnalysisResource including report.content */ }
}
}
Webhook delivery
5 max
1 min → 5 min → 30 min → 2 hr
delivery marked failed, no auto-disable
immediate failure, no retry
enforced — self-signed certs rejected
10 seconds
Return any 2xx status code to acknowledge a delivery. Any other status triggers a retry.
Webhook signatures
Every delivery is signed with HMAC-SHA256 using your endpoint's signing secret. Always verify the signature before processing a delivery — reject requests where the signature doesn't match or the timestamp is more than 5 minutes from your server clock.
X-Pinkk-Event: analysis.completed X-Pinkk-Delivery:X-Pinkk-Timestamp: X-Pinkk-Signature: t= ,v1=
signed_payload = timestamp + "." + raw_request_body signature = HMAC-SHA256(signing_secret, signed_payload) header = "t=" + timestamp + ",v1=" + hex(signature)
const crypto = require('crypto');
function verifyPinkkSignature(secret, rawBody, sigHeader, toleranceSec = 300) {
const parts = Object.fromEntries(sigHeader.split(',').map(p => p.split('=', 2)));
if (!parts.t || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}