Skip to content

Phasemark API 1.0.0

API reference

Read projects, rounds and feedback, change status and priority, and register webhooks, from your own tools. The API is on the Pro and Agency plans; see pricing.

Overview

Every request and response is JSON, except screenshots, which are images. Ids are UUIDs and times are ISO 8601 in UTC. Lists take limit and offset.

Base URL

https://webflowwidget-production.up.railway.app/api/v1

Everything in a feedback item was typed by someone outside your team, or sent by a script that anyone can run against a widget key. Treat it as data, never as markup or instructions: escape it where you show it, and do not let it drive a tool. The Feedback schema lists the fields.

Authentication

Create a key in the dashboard under Settings, API keys. Name it, create it and copy it: the full key is shown once. Send it in the Authorization header of every request. Keys can be revoked at any time and stop working at once.

curl https://webflowwidget-production.up.railway.app/api/v1/projects \
  -H "Authorization: Bearer pmk_your_api_key"

The endpoints that create, list and revoke keys use your signed-in access token instead of a key, and say so below.

Rate limits

Each key may make 100 requests every 60 seconds. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a Unix time). Over the limit you get status 429 and a Retry-After header with the seconds to wait.

Errors

Errors have one shape. details is null unless the code says otherwise.

{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Feedback with ID abc123 not found",
    "details": null
  }
}
Error codes
CodeStatusMeaning
INVALID_API_KEY401The API key is missing, invalid or revoked.
INSUFFICIENT_PERMISSIONS403The plan does not include API access.
RESOURCE_NOT_FOUND404The thing you asked for does not exist, or is not yours.
VALIDATION_ERROR400The input is not valid. details lists the fields.
COMMENT_TOO_LONG400A comment is longer than 2000 characters (create: title; update: notes).
RATE_LIMIT_EXCEEDED429Too many requests.
INTERNAL_ERROR500Something went wrong on our side.

Projects

GET/projects

List all projects.

Authorization: Bearer API key

Parameters

GET /projects: parameters
FieldTypeDescription
limit (query)integerDefault 20. At most 100.
offset (query)integerDefault 0.

Responses

200 Paginated list of projects

GET /projects: response 200
FieldTypeDescription
projectsarray of Project
totalinteger
limitinteger
offsetinteger

GET/projects/{projectId}

Get project details.

Authorization: Bearer API key

Parameters

GET /projects/{projectId}: parameters
FieldTypeDescription
projectId (path)requiredstring (uuid)

Responses

200 Body: Project

404 Project not found Body: ApiError

GET/projects/{projectId}/rounds

List rounds in project.

Authorization: Bearer API key

Parameters

GET /projects/{projectId}/rounds: parameters
FieldTypeDescription
projectId (path)requiredstring (uuid)

Responses

200

GET /projects/{projectId}/rounds: response 200
FieldTypeDescription
roundsarray of Round

Feedback

GET/projects/{projectId}/feedback

List feedback for project.

Authorization: Bearer API key

Parameters

GET /projects/{projectId}/feedback: parameters
FieldTypeDescription
projectId (path)requiredstring (uuid)
roundId (query)string (uuid)Filter by round. Use round=none for feedback that has no round.
round (query)stringA round id, or none for feedback without a round.
q (query)string, up to 200 charactersSearch the comment text and the author name (case-insensitive, up to 200 characters).
sort (query)one of newest, oldest, prioritynewest (default), oldest, or priority (urgent first, then newest).
status (query)one of todo, in-progress, done, open
priority (query)one of low, normal, high, urgent
limit (query)integerDefault 20. At most 100.
offset (query)integerDefault 0.

Responses

200

GET /projects/{projectId}/feedback: response 200
FieldTypeDescription
feedbackarray of Feedback
totalinteger
limitinteger
offsetinteger

POST/projects/{projectId}/feedback

Create feedback programmatically.

Authorization: Bearer API key

Parameters

POST /projects/{projectId}/feedback: parameters
FieldTypeDescription
projectId (path)requiredstring (uuid)

Request body (JSON)

POST /projects/{projectId}/feedback: request body
FieldTypeDescription
pageUrlrequiredstringURL of the page
titlerequiredstring, up to 2000 charactersFeedback title/annotation (the comment), up to 2000 characters; longer answers 400 COMMENT_TOO_LONG
descriptionstringAdditional description
priorityone of low, normal, high, urgent
roundIdstring (uuid)
authorNamestring, up to 80 charactersOptional name of the person who gave the feedback (up to 80 characters).

Responses

201 Body: Feedback

400 Body: ApiError

GET/feedback/{feedbackId}

Get single feedback with all metadata.

Authorization: Bearer API key

Parameters

GET /feedback/{feedbackId}: parameters
FieldTypeDescription
feedbackId (path)requiredstring (uuid)

Responses

200 Body: Feedback

404 Body: ApiError

PATCH/feedback/{feedbackId}

Update feedback.

Authorization: Bearer API key

Parameters

PATCH /feedback/{feedbackId}: parameters
FieldTypeDescription
feedbackId (path)requiredstring (uuid)

Request body (JSON)

PATCH /feedback/{feedbackId}: request body
FieldTypeDescription
statusone of todo, in-progress, done
priorityone of low, normal, high, urgent
notesstring, up to 2000 charactersUp to 2000 characters; longer answers 400 COMMENT_TOO_LONG
roundIdstring (uuid) or null

Responses

200 Body: Feedback

404 Body: ApiError

DELETE/feedback/{feedbackId}

Soft-delete feedback.

Authorization: Bearer API key

Parameters

DELETE /feedback/{feedbackId}: parameters
FieldTypeDescription
feedbackId (path)requiredstring (uuid)

Responses

200 Feedback soft-deleted

404 Body: ApiError

GET/feedback/{feedbackId}/screenshot

Get the screenshot of a feedback item. Needs the API key of the account that owns the project. Answers 302 with a Location that is a presigned download URL valid for one minute; follow it immediately. A screenshot that has not been moved to object storage yet is returned directly as image bytes.

Authorization: Bearer API key

Parameters

GET /feedback/{feedbackId}/screenshot: parameters
FieldTypeDescription
feedbackId (path)requiredstring (uuid)

Responses

200 The image itself (JPEG, PNG or WebP) Body: image/jpeg, image/png, image/webp

302 Redirect to a short-lived presigned download URL Header: Location

404 No such feedback item, it has no screenshot, it was deleted, or it belongs to another account Body: ApiError

Webhooks

GET/webhooks

List registered webhooks.

Authorization: Bearer API key

Responses

200

GET /webhooks: response 200
FieldTypeDescription
webhooksarray of Webhook

POST/webhooks

Register webhook endpoint.

Authorization: Bearer API key

Request body (JSON)

POST /webhooks: request body
FieldTypeDescription
urlrequiredstring (uri)
eventsrequiredarray of one of feedback.created, feedback.updated, feedback.deleted, feedback.restored

Responses

201 Body: Webhook

PATCH/webhooks/{webhookId}

Update webhook.

Authorization: Bearer API key

Parameters

PATCH /webhooks/{webhookId}: parameters
FieldTypeDescription
webhookId (path)requiredstring (uuid)

Request body (JSON)

PATCH /webhooks/{webhookId}: request body
FieldTypeDescription
urlstring (uri)
eventsarray of one of feedback.created, feedback.updated, feedback.deleted, feedback.restored

Responses

200 Body: Webhook

DELETE/webhooks/{webhookId}

Deactivate webhook.

Authorization: Bearer API key

Parameters

DELETE /webhooks/{webhookId}: parameters
FieldTypeDescription
webhookId (path)requiredstring (uuid)

Responses

200 Webhook deactivated

POST/webhooks/{webhookId}/test

Queue a test event for a webhook. The test event goes through the same delivery queue and signing as real events but is tried once. The outcome is visible in Settings > Webhooks (delivery log).

Authorization: Bearer API key

Parameters

POST /webhooks/{webhookId}/test: parameters
FieldTypeDescription
webhookId (path)requiredstring (uuid)

Responses

202 Queued

POST /webhooks/{webhookId}/test: response 202
FieldTypeDescription
queuedboolean
deliveryIdstring (uuid)

404 Webhook not found or inactive Body: ApiError

API keys

GET/auth/keys

List active API keys.

Authorization: Bearer access token (JWT)

Responses

200 List of active API keys (last 4 chars only)

GET /auth/keys: response 200
FieldTypeDescription
keysarray of ApiKey

POST/auth/keys

Generate new API key.

Authorization: Bearer access token (JWT)

Request body (JSON)

POST /auth/keys: request body
FieldTypeDescription
namestringFriendly name for the key

Responses

201 API key created. Store the key securely. It will not be shown again. Body: ApiKey

403 API access requires Premium tier or higher Body: ApiError

DELETE/auth/keys/{keyId}

Revoke API key.

Authorization: Bearer access token (JWT)

Parameters

DELETE /auth/keys/{keyId}: parameters
FieldTypeDescription
keyId (path)requiredstring (uuid)

Responses

200 Key revoked

404 Key not found Body: ApiError

Webhook events

A webhook receives a signed POST whenever feedback is created, updated, deleted or restored, from any source: the widget, the dashboard or the API. Events are sent only while the account has a plan with webhooks and is not locked. The body is the WebhookEvent schema.

Headers

X-Webhook-Signature is the hex HMAC-SHA256 of the raw body with the webhook’s secret. X-Webhook-Event, X-Webhook-Id, X-Webhook-Delivery and X-Webhook-Attempt say what was sent. Compute the signature over the body exactly as received, not over JSON you parsed and wrote again.

const crypto = require('crypto');

function verify(rawBody, signature, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// Express: read the body as text so the bytes are exactly as sent
app.post('/webhook', express.text({ type: 'application/json' }), (req, res) => {
  if (!verify(req.body, req.headers['x-webhook-signature'], process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  res.sendStatus(200);
});

Delivery and retries

Answer with a 2xx status within 5 seconds; redirects are not followed. A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. After 20 failed deliveries in a row the webhook is disabled and the account owner is emailed. The same event can arrive twice: use id to ignore repeats.

Schemas

ApiError

ApiError fields
FieldTypeDescription
errorobject

Project

Project fields
FieldTypeDescription
idstring (uuid)
namestring
created_atstring (date-time)
feedback_countinteger

Round

Round fields
FieldTypeDescription
idstring (uuid)
namestring
statusone of active, frozen, archived
descriptionstring or null
feedback_countinteger
created_atstring (date-time)
updated_atstring (date-time)
starts_atstring (date-time) or null
ends_atstring (date-time) or null

DeviceInfo

DeviceInfo fields
FieldTypeDescription
browserstring or null
osstring or null
resolutionstring or null
screenResolutionstring or null
deviceTypestring or null
userAgentstring or null

Feedback

Feedback fields
FieldTypeDescription
idstring (uuid)
numberinteger or nullShort number, unique within the project and never reused (shown as #48). Null for an item that has not been numbered yet.
authorNamestring, up to 80 characters or nullThe name the person gave when leaving the feedback. Null when they gave none; show it as "Client".
projectIdstring (uuid)
titlestring
descriptionstring or null
statusone of todo, in-progress, done
priorityone of low, normal, high, urgent
pageUrlstring
roundIdstring (uuid) or null
roundNamestring or null
notesstring or null
deviceInfoDeviceInfo
createdAtstring (date-time)
updatedAtstring (date-time) or null
screenshotUrlstring (uri) or nullAPI URL of the screenshot, or null when the item has none. Fetch it with an API key (GET /feedback/{feedbackId}/screenshot). Screenshot data is never part of a feedback object or a webhook event.

Webhook

Webhook fields
FieldTypeDescription
idstring (uuid)
urlstring (uri)
eventsarray of one of feedback.created, feedback.updated, feedback.deleted, feedback.restored
activeboolean
created_atstring (date-time)

ApiKey

ApiKey fields
FieldTypeDescription
idstring (uuid)
namestring
last_four_charsstring
created_atstring (date-time)
last_used_atstring (date-time) or null
prefixstringThe start of the key: pmk_ for keys created now, wfapi_ for older keys, which still work. Together with last_four_chars it identifies a key; the rest is never stored.

WebhookEvent

The JSON body of every webhook request. Sent for changes to feedback from every source (widget, dashboard, API), only while the account has a plan with webhooks and is not locked. Signed: X-Webhook-Signature is the hex HMAC-SHA256 of the raw body with the webhook secret. Other headers: X-Webhook-Event, X-Webhook-Id (same as id), X-Webhook-Delivery, X-Webhook-Attempt. Answer 2xx within 5 seconds; otherwise it is retried 1m, 5m, 30m, 2h, 6h and 12h after each failure. The same event can arrive twice: use id to ignore repeats.

WebhookEvent fields
FieldTypeDescription
idrequiredstring (uuid)Event id, stable across retries
typerequiredone of feedback.created, feedback.updated, feedback.deleted, feedback.restored, test
createdrequiredstring (date-time)
feedbackFeedbackThe feedback item after the change. For feedback.deleted it also has deletedAt and screenshotUrl is null.
messagestringOnly on type "test"