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.

Base URL https://trypinkk.com/api/v1
Rate limit 60 requests / minute per user
Versioning URL path — /api/v1/
Get started free API access is a subscriber feature.

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.

Required headers
Authorization: Bearer 
Accept: 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.

Scope Description
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.

422 example
{
  "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.

Paginated response envelope
{
  "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.

Same key + same body 200/201 — replayed. Response header Idempotency-Replayed: true.
Same key + different body 409 Conflict.
No header Normal one-shot request.

Supported on: POST /analyses, POST /profiles, POST /checks, POST /comparisons, POST /workspaces.

Rate limits

General API 60 req / min per user
POST /analyses (submit) additional per-route throttle
POST /checks (submit) additional per-route throttle

When rate limited the response is 429 Too Many Requests. Back off exponentially and retry.

Endpoints

Account

GET /user read:user

Returns the account associated with the token — plan, credit balance, remaining analysis slots, and feature limits.

200 Response
{
  "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.

GET /profiles read:profiles

List your profiles, newest first.

Query parameters
type string Filter by profile type: creator, brand, or affiliate.
per_page integer Results per page, 1–100. Default: 20.
POST /profiles write:profiles

Create a new profile. Supports Idempotency-Key.

Request body
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.
201 Response
{
  "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"
  }
}
GET /profiles/{id} read:profiles

Single profile with platforms, fetched metrics, and intelligence grade.

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

GET /analyses read:analyses

List analyses, newest first. The list omits the heavy report.content body — fetch the single-item endpoint to get full content.

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

Request body
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).
201 Response — analysis submitted
{
  "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.

GET /analyses/{id} read:analyses

Single analysis with full report.content. Content shape varies by gene — treat unknown keys defensively.

200 Response (completed)
{
  "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"
  }
}
GET /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.

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

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

200 Response
{
  "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.

GET /checks read:checks

List checks. Accepts ?status= and ?per_page=.

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

Request body
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.
201 Response — submitted for 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.

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

GET /comparisons read:comparisons

List comparisons.

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

Request body
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.
GET /comparisons/{id} read:comparisons

Single comparison with verdict and content.

Workspaces

Workspaces group profiles into named collections. A profile can belong to multiple workspaces.

GET /workspaces read:workspaces

List workspaces.

POST /workspaces write:workspaces

Create a workspace. Body: name (required), description (optional), profile_ids[] (optional). Supports Idempotency-Key.

GET /workspaces/{id} read:workspaces

Single workspace with member profiles.

PATCH /workspaces/{id} write:workspaces

Update name or description.

DELETE /workspaces/{id} write:workspaces

Delete workspace (profiles are not deleted).

POST /workspaces/{id}/profiles write:workspaces

Attach profiles. Body: { profile_ids: [ulid, …] }.

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

GET /webhooks read:webhooks

List webhook endpoints.

GET /webhooks/{id} read:webhooks

Single webhook endpoint.

DELETE /webhooks/{id} write:webhooks

Delete a webhook endpoint.

POST /webhooks write:webhooks

Register a new webhook endpoint. HTTPS only. The signing secret is returned once in the response — store it immediately.

Request body
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.
201 Response
{
  "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.
PATCH /webhooks/{id} write:webhooks

Enable or disable an endpoint, update events, or change the description. Send only the fields you want to change.

Request body
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.

Event payload envelope
{
  "id": "evt_01KS8DZMF4E2XN3G7JKWRDHQ04",
  "type": "analysis.completed",
  "created_at": "2026-05-22T13:05:00+00:00",
  "data": {
    "analysis": { /* full AnalysisResource including report.content */ }
  }
}

Webhook delivery

Attempts 5 max
Backoff schedule 1 min → 5 min → 30 min → 2 hr
After all attempts fail delivery marked failed, no auto-disable
Endpoint disabled at delivery time immediate failure, no retry
SSL verification enforced — self-signed certs rejected
Timeout per attempt 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.

Delivery headers
X-Pinkk-Event:     analysis.completed
X-Pinkk-Delivery:  
X-Pinkk-Timestamp: 
X-Pinkk-Signature: t=,v1=
Signature construction
signed_payload = timestamp + "." + raw_request_body
signature      = HMAC-SHA256(signing_secret, signed_payload)
header         = "t=" + timestamp + ",v1=" + hex(signature)
Verification example (Node.js)
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));
}