Skip to Content
APIResourcesAdmin APIMarkup

Markup

Markup is the main resource of the MarkUp.io API. Create, get, update, and delete markups (MarkUp projects); list and search by workspace; bulk create, move, and delete.

Create Markup from URL

POST /api/v2/markups/url

This URL should lead to webpage or one of supported types .

POST /api/v2/markups/url
curl "https://api.markup.io/api/v2/markups/url" \ -X POST \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json" \ --data '{ "url": "https://markup.io", "name": "My Markup" }'
200 OK
{ "data": { "id": "372c25c6-4827-4ca1-959b-5a3ea30fbb85", "createdAt": "2026-10-23T12:00:10.824Z", "modifiedAt": "2026-10-23T12:00:10.824Z", "type": "webpage", "name": "MarkUp.io", "markupUrl": "https://app.markup.io/markup/f289b90a-1166-4f63-aef3-a0cce4543413", "thumbnailUrl": "https://media.markup.io/thumbnails/markup/9b339cd0-546d-4292-a54b-6e128ce6113e", "activeThreads": 10, "readOnly": true, "status": "editing", "note": { "id": "uuid", "projectId": "uuid", "note": "Internal note", "showNoteOnProjectOpen": true }, "projectReviews": [ { "projectId": "uuid", "userId": "uuid", "comment": "Looks good", "createdAt": 1705312800000 } ], "scopes": [ "update-project-read-only", "delete-project" ], "retainOriginal": true, "hasRetainedOriginal": true, "url": "https://markup.io" } }

Response Body - MarkupResponse

Request Body CreateUrlMarkupRequest

OptionType
url string
name optionalstring

If not set the name will be set to the URL hostname.

workspaceId optionalstring

If not set or empty, the API key's workspace is used.

parentFolderId optionalstring

Folder id where the markup will be created. If not set, the workspace root folder is used.

Create Markup from file

POST /api/v2/markups/file

Create a markup from a single file in one of supported types . The endpoint takes a POST Content-Type: multipart/form-data request with one file field. The whole request must be under 20MB. For larger files, and to put several images in one markup, use the upload flow and Create Markup from files.

POST /api/v2/markups/file
curl "https://api.markup.io/api/v2/markups/file?workspaceId=123e4567-e89b-12d3-a456-426655440000" \ -X POST \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: multipart/form-data" \ -F 'file=@/path/to/pictures/picture.jpg'
200 OK
{ "data": { "id": "93a1b515-f87c-4f2c-91a4-fdfb3c042c89", "createdAt": "2026-10-23T12:00:10.824Z", "modifiedAt": "2026-10-23T12:00:10.824Z", "type": "webpage", "name": "MarkUp.io", "markupUrl": "https://app.markup.io/markup/f289b90a-1166-4f63-aef3-a0cce4543413", "thumbnailUrl": "https://media.markup.io/thumbnails/markup/9b339cd0-546d-4292-a54b-6e128ce6113e", "activeThreads": 10, "readOnly": true, "status": "editing", "note": { "id": "uuid", "projectId": "uuid", "note": "Internal note", "showNoteOnProjectOpen": true }, "projectReviews": [ { "projectId": "uuid", "userId": "uuid", "comment": "Looks good", "createdAt": 1705312800000 } ], "scopes": [ "update-project-read-only", "delete-project" ], "retainOriginal": true, "hasRetainedOriginal": true, "url": "https://markup.io" } }

Response Body - MarkupResponse

Request Query Params CreateFileMarkupRequest

OptionType
name optionalstring

If not set the name will default based on URL or file name.

workspaceId string
parentFolderId optionalstring

Folder id where the markup will be created. If not set, the workspace root folder is used.

Create Markup from files

POST /api/v2/markups/from-files

Create a markup from files already uploaded through the upload flow. Upload each file first (POST /uploads/upload-policy -> POST the bytes as a form to the returned presigned URL -> POST /uploads/upload-complete), then pass the resulting fileIds here. A file there is bounded by the workspace’s remaining storage allowance rather than by a fixed per-file cap, so it is also the way to use a file too large for Create Markup from file. Several images (png, jpeg, gif, bmp, webp, svg) become one multi-page markup, and the order of fileIds is the order they appear in. A PDF, a video, or an image needing conversion (tiff, psd, eps) makes a markup of its own, so send exactly one fileId and do not mix it with others. Every file must belong to this workspace and can be used only once.

POST /api/v2/markups/from-files
curl "https://api.markup.io/api/v2/markups/from-files" \ -X POST \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json" \ --data '{ "workspaceId": "123e4567-e89b-12d3-a456-426655440000", "fileIds": ["3f1b9d2e-1c4a-4b7e-9a1d-2c5e8f0a7b31", "8c2a4e6f-5d3b-4a1c-8e9f-0b7d6c3a2e14"], "name": "Homepage review" }'
200 OK
{ "data": { "id": "8d6f5c72-4ae8-47fb-bafb-4d17f970e71c", "createdAt": "2026-10-23T12:00:10.824Z", "modifiedAt": "2026-10-23T12:00:10.824Z", "type": "webpage", "name": "MarkUp.io", "markupUrl": "https://app.markup.io/markup/f289b90a-1166-4f63-aef3-a0cce4543413", "thumbnailUrl": "https://media.markup.io/thumbnails/markup/9b339cd0-546d-4292-a54b-6e128ce6113e", "activeThreads": 10, "readOnly": true, "status": "editing", "note": { "id": "uuid", "projectId": "uuid", "note": "Internal note", "showNoteOnProjectOpen": true }, "projectReviews": [ { "projectId": "uuid", "userId": "uuid", "comment": "Looks good", "createdAt": 1705312800000 } ], "scopes": [ "update-project-read-only", "delete-project" ], "retainOriginal": true, "hasRetainedOriginal": true, "url": "https://markup.io" } }

Response Body - MarkupResponse

Request Body CreateMarkupFromFilesRequest

OptionType
workspaceId string
fileIds string[]

Completed upload IDs. Each one is the fileId returned by Get an upload policy and then confirmed through upload-complete. Send several displayable images (png, jpeg, gif, bmp, webp, svg) to make one markup with a page per image, in the order given; or send exactly one PDF, video, or image needing conversion (tiff, psd, eps) to make a markup of that file — those types cannot be combined with each other or with images. Every file must belong to this workspace and can be used only once.

name optionalstring

If not set, the name defaults to the first file's name without its extension.

parentFolderId optionalstring

Folder id where the markup will be created. If not set, the workspace root folder is used.

Search Markups

GET /api/v2/markups/search

Search markups in workspace by name or URL (includes subfolders). Returns metadata only by default; pass include=media to attach every markup’s files, or narrow it to include=images, videos or documents.

GET /api/v2/markups/search
curl "https://api.markup.io/api/v2/markups/search?workspaceId=677e552c-f0bc-476a-9a10-ab498fb8ea94&include=media" \ -X GET \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
200 OK
{ "data": { "data": [ {} ] } }

Response Body ListMarkupResponse

OptionType
data T[]

Request Query Params SearchMarkupsQuery

OptionType
workspaceId string
query optionalstring
limit optionalnumber
include optionalstring

Comma-separated file payloads to expand, omitted by default because a page of markups can carry thousands of images. media expands every markup that has a file; images, videos and documents expand only image, video and PDF markups respectively and can be combined (images,videos). A markup of a type you did not ask for comes back without its file payload.

Get Markup Usage

GET /api/v2/markups/usage

Returns the workspace’s monthly MarkUp usage: scope, allowance, used, remaining, reset date and warning/limit flags. limit, remaining and totalRemaining are null on an unlimited plan. used counts MarkUps charged to the plan allowance and bundleUsed those charged to a purchased bundle, so the total created this period is used + bundleUsed.

GET /api/v2/markups/usage
curl "https://api.markup.io/api/v2/markups/usage" \ -X GET \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
200 OK
{ "data": { "scope": "Workspace", "used": 42, "bundleUsed": 0, "limit": 100, "remaining": 58, "isUnlimited": false, "enforcementEnabled": true, "isCustomOverride": false, "warningThreshold": 0.8, "isWarning": false, "isLimitReached": false, "bundleBalance": 0, "totalRemaining": 58, "isDrawingOnBundle": false, "averageMonthlyUsage": null, "periodStart": "2023-02-01T00:00:00.000Z", "periodEnd": "2023-03-01T00:00:00.000Z" } }

Get Markup

GET /api/v2/markups/:id

GET /api/v2/markups/:id
curl "https://api.markup.io/api/v2/markups/3a6244ea-8362-4171-b165-2fb393a8e186" \ -X GET \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
200 OK
{ "data": { "id": "c8243638-fbff-43db-ab3b-e368775f9b2d", "createdAt": "2026-10-23T12:00:10.824Z", "modifiedAt": "2026-10-23T12:00:10.824Z", "type": "webpage", "name": "MarkUp.io", "markupUrl": "https://app.markup.io/markup/f289b90a-1166-4f63-aef3-a0cce4543413", "thumbnailUrl": "https://media.markup.io/thumbnails/markup/9b339cd0-546d-4292-a54b-6e128ce6113e", "activeThreads": 10, "readOnly": true, "status": "editing", "note": { "id": "uuid", "projectId": "uuid", "note": "Internal note", "showNoteOnProjectOpen": true }, "projectReviews": [ { "projectId": "uuid", "userId": "uuid", "comment": "Looks good", "createdAt": 1705312800000 } ], "scopes": [ "update-project-read-only", "delete-project" ], "retainOriginal": true, "hasRetainedOriginal": true, "url": "https://markup.io" } }

Response Body - MarkupResponse

Request Path Params IdRequestParam

OptionType
id string

Get Markup Tag

GET /api/v2/markups/:id/tag

Returns taggable and tagged users for the markup (project). Same as v1 GET tag/project/:id.

GET /api/v2/markups/:id/tag
curl "https://api.markup.io/api/v2/markups/b07c4f8f-180c-45d1-9b64-bfda1f6e866c/tag" \ -X GET \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
200 OK
{ "data": { "taggable": [ {} ], "tagged": [ { "id": "some id", "name": "some name", "email": "some email", "avatarUrl": null } ] } }

Response Body ProjectMessageTagsResponse

OptionType
taggable TaggableTagsResponse[]
tagged

Request Path Params IdRequestParam

OptionType
id string

List Markup View Modes

GET /api/v2/markups/:id/view-modes

Returns the available view modes (device breakpoints) for a markup, with per-mode thread counts.

GET /api/v2/markups/:id/view-modes
curl "https://api.markup.io/api/v2/markups/30e94bc3-97f8-458f-b269-07a18592e770/view-modes" \ -X GET \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
Response
204 No Content

Request Path Params IdRequestParam

OptionType
id string

GET /api/v2/markups/:id/share-link

Returns the markup’s public share link, creating it if the markup does not have one yet. Anyone who opens the link joins the markup as a contributor, so this is how you hand a markup to a reviewer who has no account in the workspace. Calling it again returns the same link rather than minting a new one, and a link that has been turned off comes back as enabled: false with a null url until Update Share Link turns it back on. Creating a link requires the invite scope on the markup, which workspace- and organization-scoped API keys hold. A caller that can view the markup but not invite on it still gets an existing link, and a 404 when there is none - it never causes one to be created. Note that this call publishes the markup when no link exists yet, and on a plan that does not allow private markups that cannot be undone: Update Share Link refuses to turn a link off without that entitlement. There is currently no read-only way to ask whether a markup is already shared - scopes on Get Markup reflects the caller’s own permissions, not the markup’s sharing state - so treat this endpoint as a write when the entitlement is in doubt. Organization-scoped keys must also pass workspaceId as a query param.

GET /api/v2/markups/{id}/share-link
curl "https://api.markup.io/api/v2/markups/123e4567-e89b-12d3-a456-426655440000/share-link" \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22"
200 OK
{ "data": { "inviteId": "3f1b9d2e-1c4a-4b7e-9a1d-2c5e8f0a7b31", "enabled": true, "url": "https://app.markup.io/invite/accept/1a2b3c4d5e6f", "role": "project:contributor" } }

Response Body - ShareLinkResponse

Request Path Params IdRequestParam

OptionType
id string

PUT /api/v2/markups/:id/share-link

Turns the markup’s public share link on or off. Turning it off makes the markup private again and needs a plan that allows private markups; the request is rejected with a paywall error otherwise. Turning it back on restores the same URL rather than issuing a new one. Sending the state the link is already in succeeds and changes nothing, so a retried request is safe. enabled: false on a markup that has never been shared is also a no-op: it reports inviteId: null rather than creating a link in order to turn it off. Requires the toggle-public-invite-links scope on the markup.

PUT /api/v2/markups/{id}/share-link
curl "https://api.markup.io/api/v2/markups/123e4567-e89b-12d3-a456-426655440000/share-link" \ -X PUT \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json" \ --data '{"enabled": false}'
200 OK
{ "data": { "inviteId": "3f1b9d2e-1c4a-4b7e-9a1d-2c5e8f0a7b31", "enabled": true, "url": "https://app.markup.io/invite/accept/1a2b3c4d5e6f", "role": "project:contributor" } }

Response Body - ShareLinkResponse

Request Path Params IdRequestParam

OptionType
id string

Request Body UpdateShareLinkRequest

OptionType
enabled boolean

true to make the share link live, false to turn it off. Sending the state the link is already in succeeds and changes nothing.

List Markup Invites

GET /api/v2/markups/:id/invites

Lists the pending email invites on a markup - invites that have been sent but not yet accepted. The public share link is not an email invite and is not listed here; read it from Get Share Link. Requires the invite scope on the markup. A caller without manage-invites sees only the invites it sent itself.

GET /api/v2/markups/:id/invites
curl "https://api.markup.io/api/v2/markups/2f518e51-4c8d-4c15-a2d2-184177ac3b5d/invites" \ -X GET \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
200 OK
{ "data": { "data": [ { "id": "3f1b9d2e-1c4a-4b7e-9a1d-2c5e8f0a7b31", "email": "some email", "role": "project:contributor", "sentAt": null, "status": "sent" } ] } }

Response Body ListMarkupInvitesResponse

OptionType
data

Request Path Params IdRequestParam

OptionType
id string

Invite to Markup

POST /api/v2/markups/:id/invites

Sends email invites to a single markup. Recipients get access to this markup only - use this rather than Invite users, which makes them a member of the whole workspace. At most 25 addresses per request. The optional message appears in the invite email and requires a plan with custom invite messages. Free and trialing organizations cannot send markup invites; the request is rejected with a 403. A workspace whose invites support has disabled is also rejected with a 403. Addresses that fail individually - already has access, undeliverable domain - come back in errors while the rest are still sent, so one bad address does not discard the batch.

POST /api/v2/markups/{id}/invites
curl "https://api.markup.io/api/v2/markups/123e4567-e89b-12d3-a456-426655440000/invites" \ -X POST \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json" \ --data '{ "emails": ["[email protected]"], "message": "Please review the new homepage copy." }'
200 OK
{ "data": { "successes": [ "some string" ], "errors": [ { "email": "some email", "message": "some message" } ] } }

Response Body ISendMarkupInvitesResult

OptionType
successes string[]

Addresses the invite was sent to.

errors

Addresses that could not be invited. Empty when every address succeeded.

Request Path Params IdRequestParam

OptionType
id string

Request Body CreateMarkupInvitesRequest

OptionType
emails string[]

Addresses to invite, at most 25 per request.

message optionalstring

Optional note included in the invite email. Requires a plan with custom invite messages; the request is rejected with a paywall error otherwise.

Revoke Markup Invite

DELETE /api/v2/markups/:id/invites/:inviteId

Revokes a pending email invite on a markup. Requires the manage-invites scope, or that the caller sent the invite itself. Use Update Share Link to turn off the public share link - it is not revocable here, because making a markup private has a billing rule of its own.

DELETE /api/v2/markups/:id/invites/:inviteId
curl "https://api.markup.io/api/v2/markups/7d2314ed-59f6-4645-a141-d5dd091c30ec/invites/34004c18-c96b-45f6-aa61-0829da0e7917" \ -X DELETE \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
Response
204 No Content

Request Path Params MarkupInviteRequestParam

OptionType
id string
inviteId string

List All Markups

GET /api/v2/markups

Cursor-based pagination. Use since for next page, before for previous. Order: createdAt desc. Returns metadata only by default; pass include=media to attach every markup’s files, or narrow it to include=images, videos or documents. isReady is always reported, so a caller can tell a markup still being converted from one with no files.

GET /api/v2/markups
curl "https://api.markup.io/api/v2/markups?workspaceId=c946f0da-f0d0-40bb-a34b-7f0ede109622&include=media" \ -X GET \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
200 OK
{ "data": { "data": [ {} ], "hasMore": true } }

Response Body ListMarkupPaginatedResponse

OptionType
data
hasMore boolean
nextCursor string
prevCursor string

Request Query Params ListMarkupsQuery

OptionType
workspaceId string
limit optionalnumber
since optionalstring
before optionalstring
ttl optionalnumber
include optionalstring

Comma-separated file payloads to expand, omitted by default because a page of markups can carry thousands of images. media expands every markup that has a file; images, videos and documents expand only image, video and PDF markups respectively and can be combined (images,videos). A markup of a type you did not ask for comes back without its file payload.

Update Markup

POST /api/v2/markups/:id

POST /api/v2/markups/:id
curl "https://api.markup.io/api/v2/markups/17716a4e-aed4-4d48-a43f-cff70adf873b" \ -X POST \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json" \ --data '{ "name": "MarkUp.io" }'
200 OK
{ "data": { "id": "7424be44-9733-4ec0-a9a3-ccd62e99eafe", "createdAt": "2026-10-23T12:00:10.824Z", "modifiedAt": "2026-10-23T12:00:10.824Z", "type": "webpage", "name": "MarkUp.io", "markupUrl": "https://app.markup.io/markup/f289b90a-1166-4f63-aef3-a0cce4543413", "thumbnailUrl": "https://media.markup.io/thumbnails/markup/9b339cd0-546d-4292-a54b-6e128ce6113e", "activeThreads": 10, "readOnly": true, "status": "editing", "note": { "id": "uuid", "projectId": "uuid", "note": "Internal note", "showNoteOnProjectOpen": true }, "projectReviews": [ { "projectId": "uuid", "userId": "uuid", "comment": "Looks good", "createdAt": 1705312800000 } ], "scopes": [ "update-project-read-only", "delete-project" ], "retainOriginal": true, "hasRetainedOriginal": true, "url": "https://markup.io" } }

Response Body - MarkupResponse

Request Path Params IdRequestParam

OptionType
id string

Request Body UpdateMarkupRequest

OptionType
name string

Pause or Resume New Comments

PATCH /api/v2/markups/:id/read-only

Pauses or resumes new comments on the markup by toggling its readOnly state. Pausing requires a plan with pauseCommentsEnabled; resuming is always allowed.

PATCH /api/v2/markups/:id/read-only
curl "https://api.markup.io/api/v2/markups/a5b58a23-f59f-4fef-9bbc-f54b27471043/read-only" \ -X PATCH \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json" \ --data '{ "readOnly": true }'
200 OK
{ "data": { "id": "09a5b57a-e927-46fe-9619-f9316414bcff", "createdAt": "2026-10-23T12:00:10.824Z", "modifiedAt": "2026-10-23T12:00:10.824Z", "type": "webpage", "name": "MarkUp.io", "markupUrl": "https://app.markup.io/markup/f289b90a-1166-4f63-aef3-a0cce4543413", "thumbnailUrl": "https://media.markup.io/thumbnails/markup/9b339cd0-546d-4292-a54b-6e128ce6113e", "activeThreads": 10, "readOnly": true, "status": "editing", "note": { "id": "uuid", "projectId": "uuid", "note": "Internal note", "showNoteOnProjectOpen": true }, "projectReviews": [ { "projectId": "uuid", "userId": "uuid", "comment": "Looks good", "createdAt": 1705312800000 } ], "scopes": [ "update-project-read-only", "delete-project" ], "retainOriginal": true, "hasRetainedOriginal": true, "url": "https://markup.io" } }

Response Body - MarkupResponse

Request Path Params IdRequestParam

OptionType
id string

Request Body UpdateMarkupReadOnlyRequest

OptionType
readOnly boolean

Download Original File

GET /api/v2/markups/:id/original/download-url

Returns a short-lived presigned URL to download the markup’s retained original file. Only available for markups created from a file whose original is still kept (see hasRetainedOriginal). Returns 404 when no retained original exists.

GET /api/v2/markups/:id/original/download-url
curl "https://api.markup.io/api/v2/markups/a0daadb1-9abe-4fad-a7d6-49a64a84384e/original/download-url" \ -X GET \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
200 OK
{ "data": { "url": "some url", "filename": "some filename", "mimeType": "some mimeType", "filesize": 123 } }

Response Body OriginalDownloadUrlResponse

OptionType
url string
filename string
mimeType string
filesize number

Request Path Params IdRequestParam

OptionType
id string

Keep or Archive Original File

PATCH /api/v2/markups/:id/original

Sets the per-markup original-file retention override. retainOriginal: true keeps the original even when the workspace’s storage policy is NEVER_STORE; false archives it. An original that has already been archived cannot be re-retained (returns 400). Returns the updated markup.

PATCH /api/v2/markups/:id/original
curl "https://api.markup.io/api/v2/markups/9b2f92a6-a0af-40f2-b58f-77230add6b69/original" \ -X PATCH \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json" \ --data '{ "retainOriginal": true }'
200 OK
{ "data": { "id": "fb8e3ef3-a95a-44a8-83b3-4395f5eea870", "createdAt": "2026-10-23T12:00:10.824Z", "modifiedAt": "2026-10-23T12:00:10.824Z", "type": "webpage", "name": "MarkUp.io", "markupUrl": "https://app.markup.io/markup/f289b90a-1166-4f63-aef3-a0cce4543413", "thumbnailUrl": "https://media.markup.io/thumbnails/markup/9b339cd0-546d-4292-a54b-6e128ce6113e", "activeThreads": 10, "readOnly": true, "status": "editing", "note": { "id": "uuid", "projectId": "uuid", "note": "Internal note", "showNoteOnProjectOpen": true }, "projectReviews": [ { "projectId": "uuid", "userId": "uuid", "comment": "Looks good", "createdAt": 1705312800000 } ], "scopes": [ "update-project-read-only", "delete-project" ], "retainOriginal": true, "hasRetainedOriginal": true, "url": "https://markup.io" } }

Response Body - MarkupResponse

Request Path Params IdRequestParam

OptionType
id string

Request Body UpdateMarkupOriginalRequest

OptionType
retainOriginal boolean

Archive Original File

DELETE /api/v2/markups/:id/original

Archives the markup’s retained original file (queues it for deletion from storage) and returns the archived file size. Returns 404 when no retained original exists.

DELETE /api/v2/markups/:id/original
curl "https://api.markup.io/api/v2/markups/d3e0f420-eb86-45ab-aab5-841ff1dfe22c/original" \ -X DELETE \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
200 OK
{ "data": { "archivedFileSize": 123 } }

Response Body ArchiveOriginalResponse

OptionType
archivedFileSize number

Request Path Params IdRequestParam

OptionType
id string

Delete Markup

DELETE /api/v2/markups/:id

DELETE /api/v2/markups/:id
curl "https://api.markup.io/api/v2/markups/4b8655c9-b633-47c6-a32f-863e5847ad60" \ -X DELETE \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json"
Response
204 No Content

Request Path Params IdRequestParam

OptionType
id string

Bulk Create Markups from URLs

POST /api/v2/markups/bulk/from-url

POST /api/v2/markups/bulk/from-url
curl "https://api.markup.io/api/v2/markups/bulk/from-url" \ -X POST \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json" \ --data '{ "items": [ {} ] }'
Response
204 No Content

Request Body BulkCreateMarkupsFromUrlRequest

OptionType
items BulkCreateMarkupFromUrlItem[]

Bulk Move Markups

POST /api/v2/markups/bulk/move

POST /api/v2/markups/bulk/move
curl "https://api.markup.io/api/v2/markups/bulk/move" \ -X POST \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json" \ --data '{ "markupIds": "9d933ded-50ce-4969-9cc0-784234f5bf47", "targetFolderId": "b20bc282-351e-4b31-bdfe-e5f886ff76d8" }'
Response
204 No Content

Request Body BulkMoveMarkupsRequest

OptionType
markupIds string[]
targetFolderId string

Bulk Delete Markups

POST /api/v2/markups/bulk/delete

POST /api/v2/markups/bulk/delete
curl "https://api.markup.io/api/v2/markups/bulk/delete" \ -X POST \ -H "Authorization: Bearer <API-KEY-SECRET>" \ -H "Markup-API-Version: 2023-02-22" \ -H "Content-Type: application/json" \ --data '{ "markupIds": "84277e67-c188-49c7-b11f-c4ffa84c193c" }'
Response
204 No Content

Request Body BulkDeleteMarkupsRequest

OptionType
markupIds string[]

ShareLinkResponse

OptionType
inviteId string | null

The invite that backs the link, or null when the markup has never had one. Stable for the life of the markup - turning the link off and on again reuses it rather than minting a new one.

enabled boolean

Whether the link is live. A link that has been turned off stays off until PUT /markups/{id}/share-link turns it back on.

url string | null

The shareable URL, or null while the link is turned off. It uses the workspace's vanity domain when one is active, so it matches the links in the workspace's invite emails.

role string

The role granted to whoever opens the link. Always project:contributor.

MarkupResponse

WebpageMarkupResponse

OptionType
id string
createdAt Iso8601Timestamp
modifiedAt Iso8601Timestamp
deletedAt Iso8601Timestamp
type ProjectType
name string
markupUrl string
thumbnailUrl string
activeThreads number
readOnly boolean
status ProjectStatus
note ProjectNoteResponse
projectReviews ProjectReviewResponse[]
scopes string[]
retainOriginal boolean
hasRetainedOriginal boolean
url string

ImageMarkupResponse

OptionType
id string
createdAt Iso8601Timestamp
modifiedAt Iso8601Timestamp
deletedAt Iso8601Timestamp
type ProjectType
name string
markupUrl string
thumbnailUrl string
activeThreads number
readOnly boolean
status ProjectStatus
note ProjectNoteResponse
projectReviews ProjectReviewResponse[]
scopes string[]
retainOriginal boolean
hasRetainedOriginal boolean
originalMimeType string

Mime type of the file the markup was created from, when it differs from the images themselves (a converted document, for example). Absent for markups built directly from uploaded images, where the per-image mimeType already describes the source file.

images MarkupImage[]

The images that make up this markup, in display order. Always present on a single-markup read; on the list and search endpoints only when include=images is requested. An empty array means the markup has no images yet — check isReady to tell that from a conversion still running.

isReady boolean

Some of the files need to be converted before Markup is ready for reviewing. After the conversion is done, this flag will be set to true and the markup_ready webhook will be sent.

VideoMarkupResponse

OptionType
id string
createdAt Iso8601Timestamp
modifiedAt Iso8601Timestamp
deletedAt Iso8601Timestamp
type ProjectType
name string
markupUrl string
thumbnailUrl string
activeThreads number
readOnly boolean
status ProjectStatus
note ProjectNoteResponse
projectReviews ProjectReviewResponse[]
scopes string[]
retainOriginal boolean
hasRetainedOriginal boolean
video MarkupVideo

The video this markup was created from. Absent until the conversion job has produced it — check isReady to tell "still converting" from "no file".

isReady boolean

Some of the files need to be converted before Markup is ready for reviewing. After the conversion is done, this flag will be set to true and the markup_ready webhook will be sent.

PdfMarkupResponse

OptionType
id string
createdAt Iso8601Timestamp
modifiedAt Iso8601Timestamp
deletedAt Iso8601Timestamp
type ProjectType
name string
markupUrl string
thumbnailUrl string
activeThreads number
readOnly boolean
status ProjectStatus
note ProjectNoteResponse
projectReviews ProjectReviewResponse[]
scopes string[]
retainOriginal boolean
hasRetainedOriginal boolean
document MarkupDocument

The document this markup was created from. Absent while the upload is still being processed, so a client must not assume it is present.

isReady boolean

Some of the files need to be converted before Markup is ready for reviewing. After the conversion is done, this flag will be set to true and the markup_ready webhook will be sent.

TaggedUserTagResponse

OptionType
id string
name string
email string
avatarUrl string | null

MarkupInviteResponse

OptionType
id string

Invite id. Pass it to Revoke Markup Invite.

email string

The email address the invite was sent to.

role string

The role the invite grants once accepted. Always project:contributor.

sentAt string | null

When the invite email was sent, or null for an invite created but not yet picked up by the email worker. Such an invite is listed as soon as it is created, so it can be cancelled straight away.

status PendingInviteStatus

sent, or resent if the invite has been sent more than once. Revoked invites leave the list rather than appearing with a status, so a revocation shows up as the invite no longer being returned.

IMarkupInviteError

OptionType
email string

The address that failed.

message string

Why it failed, e.g. the recipient already has access to the markup.