Server-to-server endpoint for creating Punchline tickets from external systems (Skinlyzer feedback widget, Sentry, Posthog, etc.).
X-Punchline-Key header.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.
POST /api/v1/external/tickets
Content-Type: application/json
X-Punchline-Key: <your key>
| Field | Type | Required | Default |
|---|---|---|---|
title | string (1–500 chars) | yes | — |
description | string | no | "" |
reporterEmail | yes | — | |
reporterName | string | no | (email) |
type | BUG | FEATURE | TASK | IMPROVEMENT | IDEA | no | BUG |
priority | CRITICAL | HIGH | MEDIUM | LOW | no | MEDIUM |
pageUrl | string | no | — |
userAgent | string | no | — |
appVersion | string | no | — |
labels | array of strings (≤ 100 items, each ≤ 100 chars) | no | — |
idempotencyKey | string (≤ 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.
| Status | Body |
|---|---|
| 201 | TicketDTO of the newly created ticket |
| 200 | TicketDTO of an existing ticket — idempotent replay |
| 400 | RFC 7807 ProblemDetail (validation error) |
| 401 | RFC 7807 ProblemDetail (auth error) |
| 415 | RFC 7807 ProblemDetail (wrong content-type) |
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.
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"
}'
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();
}
| Status | When | Action |
|---|---|---|
| 401 | Missing, malformed, unknown, or revoked API key | Confirm the key, ask the admin if you need a new one |
| 400 | Body validation failed | Inspect the ProblemDetail's detail field |
| 415 | Wrong Content-Type | Set 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".
Zero-downtime as long as the new key is live before the old one is revoked.
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:
| Field | Type |
|---|---|
displayId | string (e.g. KEN-12) |
title | string |
status | string |
type | enum (see request body above) |
priority | enum |
dueDate | ISO date or null |
labels | array of label names |
closed | boolean |
publicCommentCount | integer |
reporterEmail | string |
createdAt | ISO instant |
updatedAt | ISO instant |
User identifiers, internal comments, attachment URLs, custom fields, and the activity log are intentionally omitted.
GET /api/v1/external/ticketsFiltered list of tickets your key submitted. All filters are optional and AND-composed.
| Param | Type | Notes |
|---|---|---|
reporterEmail | exact match, case-insensitive | |
status | string | exact match |
type | enum | exact match |
createdAfter | ISO instant | inclusive lower bound |
createdBefore | ISO instant | exclusive upper bound |
page | integer | default 0 |
size | integer | default 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.
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
At least one field must be non-null.
| Field | Type | Notes |
|---|---|---|
status | string | Workflow 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. |
publicSummary | string | Customer-facing summary shown on the ticket. Pass "" or blank to clear. Omit or pass null to leave unchanged. |
| Status | Body |
|---|---|
| 200 | Full TicketDTO with the updated state. |
| 400 | ProblemDetail — request body had no non-null field. |
| 401 | ProblemDetail — missing, unknown, or revoked API key. |
| 404 | Ticket not found, or not owned by this key. |
| 422 | ProblemDetail — status is not a valid workflow transition from the current status. |
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"}'
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."}'
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.