The official contract for the Xenmark Integration API — a read-only API that lets external tools read a signed-in user's Xenmark data. Xenmark is a drawing review platform for engineering and construction teams: numbered comments on PDF drawings, revision tracking, status workflow, and exports.
This repository holds the machine-readable contract and a runnable Postman collection. It is documentation, not an SDK.
| File | What it is |
|---|---|
openapi.yaml |
OpenAPI 3.0 description of every live route, with the response envelope, error codes, scopes, and cursor contracts |
collection/xenmark-integration-api.postman_collection.json |
The same API as a Postman collection — every request documented, placeholder variables, no credentials. Also published on the Postman API Network |
CHANGELOG.md |
API releases as they reach this contract |
SECURITY.md |
How to report a security issue |
Product and documentation: https://www.xenmark.app/integrations/
Every consumer uses a token the user creates inside Xenmark. Your Xenmark password is never entered anywhere else.
Personal API token (Postman, scripts, your own tools): in Xenmark, open your profile → Account & Security → Connected apps → Connect an app → "Personal API token (Postman, GitHub)" → Generate. The token (xmk_live_…) is shown once. It carries the full read menu — notifications:read, projects:read, review:read — is revocable at any time from Connected apps, and each user may hold up to 5.
Official Xenmark apps (the browser extensions) pair instead with a one-time connect code redeemed at POST /v1/auth/pair/claim. That path is restricted to pre-approved official app contexts; it is documented here for completeness, not for third-party use.
Send the token as a bearer header on every request:
Authorization: Bearer xmk_live_YOUR_TOKEN_HERE
https://api.xenmark.app
- Envelope. Every success response is
{ "data": …, "meta": { "next_cursor", "has_more" } }. - Errors. Always
{ "error": { "code", "message" } }. Codes:invalid_token,insufficient_scope,not_found,invalid_request,rate_limited,service_disabled. Every authentication failure is the identicalinvalid_token. - Pagination. Keyset cursors: pass
meta.next_cursorback as?cursor=. Collections page newest-first./v1/notificationsuses its own forward high-water?after=contract — see the route. - Rate limits. 120 requests/hour and 20/minute per token;
429carriesRetry-After. Poll/v1/notificationsno more often than every 60 seconds. - Not-found is neutral. A resource you cannot access returns the same
404 not_foundas one that does not exist. - Forward compatibility. Responses may gain fields over time — ignore unknown fields; any field can be null. New
event_typevalues can appear — render unknown types generically. - Read-only, permanently. The API never writes. The read menu grows additively, each new item behind its own scope.
- Every response is
Cache-Control: no-store.
| Route | Scope | Returns |
|---|---|---|
POST /v1/auth/pair/claim |
none (official apps only) | one-time token claim from a connect code |
GET /v1/me |
any valid token | user_id, display_name, company — never e-mail, plan, billing, or role |
GET /v1/notifications |
notifications:read |
the user's notification inbox, each row with a ready-made web_url deep link |
GET /v1/projects |
projects:read |
projects the user owns or is a member of |
GET /v1/projects/{id}/drawings |
projects:read |
drawings in a project |
GET /v1/drawings/{id}/revisions |
projects:read |
revisions of a drawing (never storage locations) |
GET /v1/revisions/{id}/summary |
review:read |
comment-status rollup for one revision |
GET /v1/revisions/summary?ids= · POST /v1/revisions/summary |
review:read |
batch summaries (25 by query, 100 by body); inaccessible ids are silently omitted |
GET /v1/revisions/{id}/comments |
review:read |
comments on a revision, with a replies preview |
GET /v1/comments/{id}/replies |
review:read |
paginated replies for one comment |
GET /v1/notifications is designed for a quiet once-a-minute poll.
- First call, no
after: returns the newest rows (newest-first) as a recent list; storemeta.next_cursoras your watermark and notify nothing. - Every call after: send
?after=<cursor>; you receive strictly-newer rows, oldest-first. Repeat immediately whilemeta.has_moreis true, then store the new cursor. - An empty
dataarray with the cursor echoed back is the normal quiet state. - A
400on the cursor means it is no longer valid: drop it and start again from step 1.
Event types today: comment.added, reply.added, comment.mention, reply.mention, comment.status_changed, project.owner_account_deleted. Rows carry actor_name, project_name, drawing_number, revision_label, comment_number, title, preview, mention_labels, old_status/new_status (status changes only), read, created_at, entity ids, and web_url. Never construct Xenmark URLs yourself — use web_url.
E-mail addresses, files or file storage locations, billing or subscription data, account roles, or anything from projects the user is not a member of.
Import collection/xenmark-integration-api.postman_collection.json into Postman, set the token collection variable to a Personal API token, and send Identity → Who am I. Every request in the collection is documented inline. The same collection is live on the Postman API Network: https://www.postman.com/xenmarkapp/xenmark
Questions about the API or your account: support@xenmark.app. Security issues: see SECURITY.md. Never post a token or connect code anywhere public. This repository does not use GitHub Issues.
The contents of this repository — the OpenAPI specification, the Postman collection, and these notes — are © Xenmark AS and licensed under the Creative Commons Attribution 4.0 International licence (see LICENSE). You may copy and adapt them, including into your own tools, with attribution to Xenmark. The licence covers this documentation only; it grants no rights to the Xenmark service, software, or trademarks.