← Punchline
API docs/External Tickets

External Ticket Creation API

Server-to-server endpoint for creating Punchline tickets from external systems (Skinlyzer feedback widget, Sentry, Posthog, etc.).

Overview

Authentication

A Punchline project admin issues you a key from project settings. The plaintext is shown exactly once at creation; store it as a secret.

X-Punchline-Key: pnchl_live_a1B2c3D4e5F6g7H8i9J0kLmNoPqRsTuVwXy

If a key is leaked or no longer needed, the admin revokes it from the same settings page. Revocation takes effect immediately.

Endpoint

POST /api/v1/external/tickets
Content-Type: application/json
X-Punchline-Key: <your key>

Request body

FieldTypeRequiredDefault
titlestring (1–500 chars)yes
descriptionstringno""
reporterEmailemailyes
reporterNamestringno(email)
typeBUG | FEATURE | TASK | IMPROVEMENT | IDEAnoBUG
priorityCRITICAL | HIGH | MEDIUM | LOWnoMEDIUM
pageUrlstringno
userAgentstringno
appVersionstringno
labelsarray of strings (≤ 100 items, each ≤ 100 chars)no
idempotencyKeystring (≤ 255 chars)no

pageUrl, userAgent, and appVersion are formatted into a Markdown "Context" block prepended to the description.

labels is an array of label names (e.g. ["billing", "mobile"]). Labels that don't exist yet are created automatically with a neutral colour. If the project has auto-assignment enabled, Punchline uses these labels to route the ticket to the best-matched AGENT (by tier + skill labels, round-robin). Pass labels that describe the topic of the ticket; routing runs silently in the background and does not affect the response.

Responses

StatusBody
201TicketDTO of the newly created ticket
200TicketDTO of an existing ticket — idempotent replay
400RFC 7807 ProblemDetail (validation error)
401RFC 7807 ProblemDetail (auth error)
415RFC 7807 ProblemDetail (wrong content-type)

Idempotency

If you include idempotencyKey (recommended: a UUID per submission), Punchline guarantees that retries with the same key + same API key never create more than one ticket. The second call returns the original ticket with HTTP 200.

The dedup window is the lifetime of the project — keys are not currently expired. A future cleanup job will prune entries older than 7 days; design for that.

Sample: cURL

curl -X POST https://punchline.example.com/api/v1/external/tickets \
  -H "X-Punchline-Key: pnchl_live_a1B2c3D4e5F6g7H8i9J0kLmNoPqRsTuVwXy" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Login button broken on Safari",
    "description": "Clicking does nothing.",
    "reporterEmail": "alice@example.com",
    "reporterName": "Alice Smith",
    "type": "BUG",
    "priority": "HIGH",
    "pageUrl": "https://app.example.com/login",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
    "appVersion": "2.4.1",
    "idempotencyKey": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
  }'

Sample: Node fetch

import { randomUUID } from 'node:crypto';

async function createTicket(report) {
  const res = await fetch('https://punchline.example.com/api/v1/external/tickets', {
    method: 'POST',
    headers: {
      'X-Punchline-Key': process.env.PUNCHLINE_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      title: report.title,
      description: report.body,
      reporterEmail: report.user.email,
      reporterName: report.user.name,
      type: 'BUG',
      priority: 'MEDIUM',
      pageUrl: report.context.url,
      userAgent: report.context.userAgent,
      appVersion: report.context.version,
      idempotencyKey: randomUUID(),
    }),
  });
  if (!res.ok) {
    throw new Error(`Punchline ${res.status}: ${await res.text()}`);
  }
  return res.json();
}

Errors

StatusWhenAction
401Missing, malformed, unknown, or revoked API keyConfirm the key, ask the admin if you need a new one
400Body validation failedInspect the ProblemDetail's detail field
415Wrong Content-TypeSet Content-Type: application/json

The 401 response is intentionally identical for all four causes so attackers cannot distinguish "key doesn't exist" from "key was revoked".

Key rotation

  1. Ask a Punchline admin to issue a new key.
  2. Deploy the new key to your service.
  3. Ask the admin to revoke the old key.

Zero-downtime as long as the new key is live before the old one is revoked.

Following up on tickets

After creating a ticket, the same API key can read its state. Two read-only endpoints, both scoped to tickets this key submitted via the create endpoint above. Tickets created by other means (the internal UI, email-intake, or a different API key) return 404.

GET /api/v1/external/tickets/{displayId}

Returns the public-safe ticket state.

GET /api/v1/external/tickets/KEN-12
X-Punchline-Key: <your key>

Response body:

FieldType
displayIdstring (e.g. KEN-12)
titlestring
statusstring
typeenum (see request body above)
priorityenum
dueDateISO date or null
labelsarray of label names
closedboolean
publicCommentCountinteger
reporterEmailstring
createdAtISO instant
updatedAtISO instant

User identifiers, internal comments, attachment URLs, custom fields, and the activity log are intentionally omitted.

GET /api/v1/external/tickets

Filtered list of tickets your key submitted. All filters are optional and AND-composed.

ParamTypeNotes
reporterEmailemailexact match, case-insensitive
statusstringexact match
typeenumexact match
createdAfterISO instantinclusive lower bound
createdBeforeISO instantexclusive upper bound
pageintegerdefault 0
sizeintegerdefault 20, max 100

Returns a Spring Page wrapper with content, totalElements, totalPages, number, size. Each item has the same shape as the single-ticket response above.

Updating a ticket

After creating a ticket, the same API key can update its workflow status and/or its customer-facing public summary. Only tickets this key submitted are accessible.

PATCH /api/v1/external/tickets/{displayId}

PATCH /api/v1/external/tickets/KEN-12
X-Punchline-Key: <your key>
Content-Type: application/json

Request body

At least one field must be non-null.

FieldTypeNotes
statusstringWorkflow status name (e.g. "In Progress", "Done"). Must be a valid transition from the current status; invalid transitions return 422. Omit or pass null to leave unchanged.
publicSummarystringCustomer-facing summary shown on the ticket. Pass "" or blank to clear. Omit or pass null to leave unchanged.

Responses

StatusBody
200Full TicketDTO with the updated state.
400ProblemDetail — request body had no non-null field.
401ProblemDetail — missing, unknown, or revoked API key.
404Ticket not found, or not owned by this key.
422ProblemDetailstatus is not a valid workflow transition from the current status.

Sample: move a ticket to "In Progress"

curl -X PATCH https://punchline.example.com/api/v1/external/tickets/KEN-12 \
  -H "X-Punchline-Key: pnchl_live_a1B2c3D4e5F6g7H8i9J0kLmNoPqRsTuVwXy" \
  -H "Content-Type: application/json" \
  -d '{"status": "In Progress"}'

Sample: set a public summary

curl -X PATCH https://punchline.example.com/api/v1/external/tickets/KEN-12 \
  -H "X-Punchline-Key: pnchl_live_a1B2c3D4e5F6g7H8i9J0kLmNoPqRsTuVwXy" \
  -H "Content-Type: application/json" \
  -d '{"publicSummary": "We have identified the root cause and a fix is in progress."}'

Workflow transitions

Status names come from the project's configured workflow. Default transitions:

OPEN → In Progress, Done In Progress → Review, Open, Done Review → Done, In Progress Done → Open

Ask a Punchline admin to share the exact status names for your project. An invalid transition returns 422 with a description of the allowed next states.