HomeDocsChangelog
Documentation

CADLens API

The CADLens API turns DWG, DXF, and DWF files into structured JSON your code can render, diff, and route.

This is a REST API. Authenticate with a bearer API key, upload a file, poll the job until it finishes (or wait for a webhook), then read a JSON document with vector entities, layers, drawing metadata, and a rendered PNG preview.

All endpoints live under the /v1 path prefix. Stable formats: DWG, DXF, DWF. Beta formats: PDF, DWFx, DGN (V7). File size limit is 10 MB on the free plan, and 25 MB–250 MB depending on your paid plan (1 GB on Enterprise). The file type is verified by magic bytes, not the extension.

How it works#

  1. Upload. POST a file to /v1/parse. You get a job back with status PENDING.
  2. Wait. Either poll /v1/jobs/{jobId} or register a webhook.
  3. Read. When status is COMPLETED, GET /v1/jobs/{jobId}/result for the vector JSON and preview.
i
No SDK required. CADLens is HTTP and JSON. The examples on this page are curl, Node 18+ fetch, Python, and Go; port them to whatever stack you run.
Base URL
https://api.cadlens.co/v1
Authentication
Authorization: Bearer cadl_...

Quickstart#

From zero to your first vectorized response in about two minutes. No SDK to install.

1. Get an API key#

Request beta access at cadlens.co. Once approved (within 48 h), sign in to the dashboard to create and copy your cadl_… key — the full key is shown only once. Stash it in your environment as CADLENS_KEY. See Authentication for the rules.

2. POST your file#

Multipart upload to /v1/parse with a file field (up to your plan limit — 10 MB on free). The response is a job — in the default async mode its status is PENDING until the worker picks it up.

3. Wait for the result#

Either poll GET /v1/jobs/{jobId} until status === "COMPLETED", or skip polling and use webhooks. Then GET /v1/jobs/{jobId}/result to read the parsed JSON and preview URL.

!
Upload limit is 10 MB on the free plan (higher on paid plans). Accepted formats: DWG, DXF, DWF (stable) and PDF, DWFx, DGN V7 (beta). See errors for the codes returned when a file is rejected.
Set your key
export CADLENS_KEY="cadl_••••••••••••••••••••••••••••••••••••••••••••••••"
Send a file
curl -X POST https://api.cadlens.co/v1/parse \
  -H "Authorization: Bearer $CADLENS_KEY" \
  -F "[email protected]" \
  -F "mode=async"
Wait & read
curl https://api.cadlens.co/v1/jobs/job_lH9k2c \
  -H "Authorization: Bearer $CADLENS_KEY"

Authentication#

Every request to the parse and jobs endpoints uses a bearer API key in the Authorization header:

Authorization: Bearer cadl_<48 hex characters>

Keys are created in the dashboard and start with the prefix cadl_. The full key is shown once at creation — store it securely; only the first few characters (the key prefix) are visible afterwards. Revoking a key in the dashboard takes effect immediately. A request with a missing, malformed, revoked, or expired key returns 401.

!
Never call CADLens from a browser. Anything you POST from fetch() in the front-end leaks your key. Proxy through your backend.
Authenticated request
curl https://api.cadlens.co/v1/jobs/job_lH9k2c \
  -H "Authorization: Bearer cadl_•••••"

Parse a file#

POST/v1/parse

Upload a CAD file as multipart/form-data. Every parse returns both vector JSON and a rendered PNG preview. In the default async mode the response is 202 with a job_id and status PENDING — poll GET /v1/jobs/{jobId} or use a webhook until the job is COMPLETED.

Accepted formats: DWG, DXF, DWF (stable) and PDF, DWFx, DGN V7 (beta). Maximum file size depends on your plan — 10 MB on the free tier, up to 100 MB on paid plans. The type is verified by magic bytes, not the file extension.

Body#

FieldTypeDescription
file
required
fileMultipart file field. The CAD drawing to parse.
webhookUrl
optional
string<url>Per-call webhook to POST when the job completes or fails. Overrides any saved endpoint.
mode
optional
enum`async` returns 202 + job_id immediately. `sync` holds the request until the worker finishes — but large files (≥10 MB, or drawings that convert to very large geometry) return 202 immediately with a `message` field and continue processing asynchronously; poll the job or use a webhook. Prefer `async` for large files. · default async
Request
curl -X POST https://api.cadlens.co/v1/parse \
  -H "Authorization: Bearer $CADLENS_KEY" \
  -F "[email protected]" \
  -F "mode=async"
Response
{
  "job_id": "job_lH9k2c",
  "status": "PENDING",
  "fileName": "floor-plan.dwg",
  "fileSize": 184320,
  "createdAt": "2026-05-21T10:42:11.000Z"
}

Get a job#

GET/v1/jobs/{jobId}

Read the current status of a job. Idempotent and not counted against your quota — safe to poll. status is one of PENDING, PROCESSING, COMPLETED, or FAILED. imageUrl is a signed PNG URL once COMPLETED, otherwise null.

Path parameters#

FieldTypeDescription
jobId
required
stringJob ID returned from `/v1/parse` (prefix `job_`).
Request
curl https://api.cadlens.co/v1/jobs/job_lH9k2c \
  -H "Authorization: Bearer $CADLENS_KEY"
Response
{
  "id": "job_lH9k2c",
  "uuid": "job_lH9k2c",
  "status": "COMPLETED",
  "fileName": "floor-plan.dwg",
  "fileSize": 184320,
  "createdAt": "2026-05-21T10:42:11.000Z",
  "startedAt": "2026-05-21T10:42:12.000Z",
  "completedAt": "2026-05-21T10:42:19.000Z",
  "errorMsg": null,
  "imageUrl": "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c.png?X-Amz-Expires=3600&..."
}

Get the result#

GET/v1/jobs/{jobId}/result

Returns the parsed document as a sheets[] array. Each sheet contains its own entities (geometry), layers (only those used on that sheet), a signed imageUrl, and spatial stats (boundingBox, area, perimeter). Top-level file and summary fields give file metadata and aggregate counts. Available only once the job is COMPLETED — calling it earlier returns 400 "Job result is not yet available".

Path parameters#

FieldTypeDescription
jobId
required
stringJob ID returned from `/v1/parse` (prefix `job_`).
Request
curl https://api.cadlens.co/v1/jobs/job_lH9k2c/result \
  -H "Authorization: Bearer $CADLENS_KEY"
Response
{
  "schemaVersion": "2.0.0",
  "parserVersion": "2.0.0",
  "jobId": "job_lH9k2c",
  "status": "COMPLETED",
  "file": {
    "name": "floor-plan.dwg",
    "format": "DWG",
    "version": "AC1032",
    "units": "mm"
  },
  "summary": {
    "totalSheets": 3,
    "totalEntities": 1284,
    "totalLayers": 8,
    "statistics": {
      "byType": { "LINE": 720, "LWPOLYLINE": 301, "TEXT": 148, "INSERT": 115 },
      "byCategory": { "Geometry": 1021, "Annotation": 148, "BlockReference": 115 }
    },
    "boundingBox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 7200, "width": 12000, "height": 7200 },
    "truncated": false
  },
  "sheets": [
    {
      "name": "M-2",
      "key": "m-2",
      "index": 0,
      "imageUrl": "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-0.png?X-Amz-Expires=3600&...",
      "entityCount": 412,
      "layerCount": 2,
      "boundingBox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 7200, "width": 12000, "height": 7200 },
      "area": 86400000,
      "perimeter": 38400,
      "layers": [
        { "name": "WALL", "color": 7, "colorHex": "#FFFFFF", "lineType": "CONTINUOUS", "isVisible": true, "entityCount": 374 },
        { "name": "DOOR", "color": 3, "colorHex": "#00FF00", "lineType": "CONTINUOUS", "isVisible": true, "entityCount": 38 }
      ],
      "entities": [
        {
          "id": "1A4",
          "handle": "1A4",
          "type": "LINE",
          "category": "Geometry",
          "layer": "WALL",
          "layout": "M-2",
          "geometry": { "start": { "x": 0, "y": 0 }, "end": { "x": 12000, "y": 0 } },
          "text": null,
          "reference": null,
          "properties": { "colorIndex": 7, "lineType": null, "lineweight": null, "visible": true, "solid": null, "patternName": null, "patternAngle": null, "patternScale": null },
          "bbox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 0 },
          "metrics": { "length": 12000, "area": null, "perimeter": null, "vertexCount": 2 }
        },
        {
          "id": "1B2",
          "handle": "1B2",
          "type": "LWPOLYLINE",
          "category": "Geometry",
          "layer": "WALL",
          "layout": "M-2",
          "geometry": { "vertices": [{ "x": 0, "y": 0 }, { "x": 12000, "y": 0 }, { "x": 12000, "y": 7200 }, { "x": 0, "y": 7200 }], "closed": true, "filled": null },
          "text": null,
          "reference": null,
          "properties": { "colorIndex": 7, "lineType": null, "lineweight": null, "visible": true, "solid": null, "patternName": null, "patternAngle": null, "patternScale": null },
          "bbox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 7200 },
          "metrics": { "length": 38400, "area": 86400000, "perimeter": 38400, "vertexCount": 4 }
        }
      ]
    }
  ],
  "metadata": {
    "filename": "floor-plan.dwg",
    "format": "DWG",
    "dwgVersion": "AC1032",
    "units": "mm",
    "boundingBox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 7200, "width": 12000, "height": 7200 }
  },
  "parseInfo": { "durationMs": 812, "warnings": [], "errors": [] },
  "imageUrl": "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-0.png?X-Amz-Expires=3600&...",
  "imageUrls": [
    "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-0.png?X-Amz-Expires=3600&...",
    "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-1.png?X-Amz-Expires=3600&...",
    "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-2.png?X-Amz-Expires=3600&..."
  ],
  "createdAt": "2026-05-21T10:42:19.000Z"
}

Get the preview image#

GET/v1/jobs/{jobId}/image

Returns a JSON object with imageUrl — a signed S3 URL to the rendered PNG preview, valid for one hour. The same URL is also included in the job and result responses. Available only once the job is COMPLETED.

Path parameters#

FieldTypeDescription
jobId
required
stringJob ID returned from `/v1/parse` (prefix `job_`).
Request
curl https://api.cadlens.co/v1/jobs/job_lH9k2c/image \
  -H "Authorization: Bearer $CADLENS_KEY"
Response
{
  "imageUrl": "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c.png?X-Amz-Expires=3600&..."
}

List jobs#

GET/v1/jobs

Returns the jobs created by the calling API key — up to the 100 most recent, newest first. Takes no query parameters.

Request
curl https://api.cadlens.co/v1/jobs \
  -H "Authorization: Bearer $CADLENS_KEY"
Response
{
  "jobs": [
    {
      "id": "job_lH9k2c",
      "uuid": "job_lH9k2c",
      "status": "COMPLETED",
      "fileName": "floor-plan.dwg",
      "fileSize": 184320,
      "createdAt": "2026-05-21T10:42:11.000Z",
      "startedAt": "2026-05-21T10:42:12.000Z",
      "completedAt": "2026-05-21T10:42:19.000Z",
      "errorMsg": null,
      "imageUrl": "https://s3.amazonaws.com/.../job_lH9k2c.png?X-Amz-Expires=3600&..."
    }
  ]
}

Delete a job#

DELETE/v1/jobs/{jobId}

Permanently deletes the job and its stored artifacts (vector JSON and the rendered PNG). Returns 204 No Content on success, or 404 if the job does not exist for your API key. This cannot be undone.

Path parameters#

FieldTypeDescription
jobId
required
stringJob ID returned from `/v1/parse` (prefix `job_`).
Request
curl -X DELETE https://api.cadlens.co/v1/jobs/job_lH9k2c \
  -H "Authorization: Bearer $CADLENS_KEY"
Response
(empty body)

API Playground#

A worked example of a single parse, end to end. Follow the three steps alongside — upload a file, poll the job until it's COMPLETED, then read the result.

  1. Upload. POST /v1/parse with your file returns a job_id and status PENDING.
  2. Poll. GET /v1/jobs/{jobId} until the status is COMPLETED (or FAILED).
  3. Read. GET /v1/jobs/{jobId}/result returns the vector JSON, layers, metadata, and a preview URL.
i
This is a read-only example. To run requests interactively against your own account, open the Playground in your dashboard after creating an API key.
1 · Upload
curl -X POST https://api.cadlens.co/v1/parse \
  -H "Authorization: Bearer $CADLENS_KEY" \
  -F "[email protected]" \
  -F "mode=async"
{
  "job_id": "job_lH9k2c",
  "status": "PENDING",
  "fileName": "floor-plan.dwg",
  "fileSize": 184320,
  "createdAt": "2026-05-21T10:42:11.000Z"
}
2 · Poll until done
curl https://api.cadlens.co/v1/jobs/job_lH9k2c \
  -H "Authorization: Bearer $CADLENS_KEY"
{
  "id": "job_lH9k2c",
  "uuid": "job_lH9k2c",
  "status": "COMPLETED",
  "fileName": "floor-plan.dwg",
  "fileSize": 184320,
  "createdAt": "2026-05-21T10:42:11.000Z",
  "startedAt": "2026-05-21T10:42:12.000Z",
  "completedAt": "2026-05-21T10:42:19.000Z",
  "errorMsg": null,
  "imageUrl": "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c.png?X-Amz-Expires=3600&..."
}
3 · Read the result
curl https://api.cadlens.co/v1/jobs/job_lH9k2c/result \
  -H "Authorization: Bearer $CADLENS_KEY"
{
  "schemaVersion": "2.0.0",
  "parserVersion": "2.0.0",
  "jobId": "job_lH9k2c",
  "status": "COMPLETED",
  "file": {
    "name": "floor-plan.dwg",
    "format": "DWG",
    "version": "AC1032",
    "units": "mm"
  },
  "summary": {
    "totalSheets": 3,
    "totalEntities": 1284,
    "totalLayers": 8,
    "statistics": {
      "byType": { "LINE": 720, "LWPOLYLINE": 301, "TEXT": 148, "INSERT": 115 },
      "byCategory": { "Geometry": 1021, "Annotation": 148, "BlockReference": 115 }
    },
    "boundingBox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 7200, "width": 12000, "height": 7200 },
    "truncated": false
  },
  "sheets": [
    {
      "name": "M-2",
      "key": "m-2",
      "index": 0,
      "imageUrl": "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-0.png?X-Amz-Expires=3600&...",
      "entityCount": 412,
      "layerCount": 2,
      "boundingBox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 7200, "width": 12000, "height": 7200 },
      "area": 86400000,
      "perimeter": 38400,
      "layers": [
        { "name": "WALL", "color": 7, "colorHex": "#FFFFFF", "lineType": "CONTINUOUS", "isVisible": true, "entityCount": 374 },
        { "name": "DOOR", "color": 3, "colorHex": "#00FF00", "lineType": "CONTINUOUS", "isVisible": true, "entityCount": 38 }
      ],
      "entities": [
        {
          "id": "1A4",
          "handle": "1A4",
          "type": "LINE",
          "category": "Geometry",
          "layer": "WALL",
          "layout": "M-2",
          "geometry": { "start": { "x": 0, "y": 0 }, "end": { "x": 12000, "y": 0 } },
          "text": null,
          "reference": null,
          "properties": { "colorIndex": 7, "lineType": null, "lineweight": null, "visible": true, "solid": null, "patternName": null, "patternAngle": null, "patternScale": null },
          "bbox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 0 },
          "metrics": { "length": 12000, "area": null, "perimeter": null, "vertexCount": 2 }
        },
        {
          "id": "1B2",
          "handle": "1B2",
          "type": "LWPOLYLINE",
          "category": "Geometry",
          "layer": "WALL",
          "layout": "M-2",
          "geometry": { "vertices": [{ "x": 0, "y": 0 }, { "x": 12000, "y": 0 }, { "x": 12000, "y": 7200 }, { "x": 0, "y": 7200 }], "closed": true, "filled": null },
          "text": null,
          "reference": null,
          "properties": { "colorIndex": 7, "lineType": null, "lineweight": null, "visible": true, "solid": null, "patternName": null, "patternAngle": null, "patternScale": null },
          "bbox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 7200 },
          "metrics": { "length": 38400, "area": 86400000, "perimeter": 38400, "vertexCount": 4 }
        }
      ]
    }
  ],
  "metadata": {
    "filename": "floor-plan.dwg",
    "format": "DWG",
    "dwgVersion": "AC1032",
    "units": "mm",
    "boundingBox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 7200, "width": 12000, "height": 7200 }
  },
  "parseInfo": { "durationMs": 812, "warnings": [], "errors": [] },
  "imageUrl": "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-0.png?X-Amz-Expires=3600&...",
  "imageUrls": [
    "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-0.png?X-Amz-Expires=3600&...",
    "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-1.png?X-Amz-Expires=3600&...",
    "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-2.png?X-Amz-Expires=3600&..."
  ],
  "createdAt": "2026-05-21T10:42:19.000Z"
}

Jobs & polling#

Parsing is asynchronous. Every POST /v1/parse returns a job with a status that walks one of two paths:

PENDING  →  PROCESSING  →  COMPLETED  ┐
                                      ├─ terminal
PENDING  →  PROCESSING  →  FAILED     ┘

Polling. Call GET /v1/jobs/{jobId} until status is COMPLETED or FAILED. Use exponential backoff starting around 250ms and capped at ~4s — the example alongside does this. Polling does not count against your usage quota. Prefer a webhook if you'd rather not poll.

Glossary#

Job
A single parse request. Created by POST /v1/parse and identified by a job_ ID.
Status
One of PENDING, PROCESSING, COMPLETED, or FAILED.
Terminal state
A status that no longer changes — COMPLETED or FAILED. Stop polling once reached.
Polling
Repeatedly checking a job's status until it reaches a terminal state.
Webhook
An alternative to polling: CADLens calls your URL when the job progresses. See Webhooks.
Preview
A rendered PNG of the drawing, available via a signed URL once the job is COMPLETED.

For the exact fields of a job and result object, see the Get job and Get result reference.

Poll a job
curl https://api.cadlens.co/v1/jobs/job_lH9k2c \
  -H "Authorization: Bearer $CADLENS_KEY"

Result schema#

When a job is COMPLETED, its result is organised as a list of sheets. Each sheet carries its own entities, layers, bounding box, area, and preview image URL — so multi-sheet DWG/DXF files are fully navigable without any client-side filtering. Top-level file and summary fields give file-level metadata and aggregate counts at a glance.

Glossary#

file
File-level metadata: original filename, format (DWG/DXF/DWF), CAD version string, and drawing units.
summary
Aggregate counts across all sheets: totalSheets, totalEntities, totalLayers, overall bounding box, and whether the result was truncated.
sheets
Array of layout/sheet objects. Each sheet has its own entities (geometry) and layers (only those used on that sheet), a signed imageUrl, and spatial stats (boundingBox, area, perimeter). See Vector entity types.
Layer
A named group entities belong to, with a colour, line type, and visibility — mirroring the source CAD layers.
Metadata
Drawing-level information carried for backward compatibility: file name, source format, drawing units, and overall extent.
Bounding box
The rectangular extent of a sheet (or the whole drawing in summary) — width and height in drawing units.
Units
The drawing's measurement unit — millimetres, centimetres, metres, inches, feet, or unknown.
Preview image
A rendered PNG per sheet, returned as a signed URL valid for one hour.

For the exact field names and types, see the Get result reference.

Result, at a glance
{
  "schemaVersion": "2.0.0",
  "parserVersion": "2.0.0",
  "jobId": "job_...",
  "status": "COMPLETED",
  "file":    { "name", "format", "version", "units" },
  "summary": {
    "totalSheets", "totalEntities", "totalLayers",
    "statistics": { "byType": { ... }, "byCategory": { ... } },
    "boundingBox", "truncated"
  },
  "sheets": [
    {
      "name": "M-2",
      "key": "m-2",
      "index": 0,
      "imageUrl": "https://.../preview-0.png",
      "entityCount": 412,
      "layerCount": 5,
      "boundingBox": { ... },
      "area": 86400000,
      "perimeter": 38400,
      "layers":   [ /* layers used in this sheet */ ],
      "entities": [ /* id, handle, type, category, layer, layout,
                       geometry, text, reference, properties,
                       bbox, metrics */ ]
    }
  ],
  "metadata": { /* file & drawing info */ },
  "parseInfo": { "durationMs", "warnings": [], "errors": [] },
  "imageUrl":  "https://.../preview-0.png",
  "imageUrls": [ /* one URL per sheet */ ],
  "createdAt": "..."
}

Vector entity types#

Each entry in sheets[n].entities holds one geometric object from that sheet, wrapped in the Schema v2 envelope: identity (id, handle, type, category, layer, layout), the spatial data in geometry (2D points in the drawing's own units, original precision), and always-present computed helpers bbox and metrics (rounded to 6 decimals, null where not applicable). text is populated for TEXT/MTEXT and reference.blockName for INSERT — both are null on other types.

Geometry per type#

typecategorygeometry fieldsmetrics
LINEGeometrystart, endlength, vertexCount
ARCGeometrycenter, radius, startAngle, endAngle (radians)length
CIRCLEGeometrycenter, radiusarea, perimeter
POLYLINE / LWPOLYLINEGeometryvertices (with optional bulge), closed, filledlength, vertexCount; area + perimeter when closed
TEXT / MTEXTAnnotationposition, rotation (degrees) — content in the text sibling
INSERTBlockReferenceposition, scaleX, scaleY, rotation (degrees) — block name in reference
SPLINEGeometrycontrolPoints, degree, knotsvertexCount
ELLIPSEGeometrycenter, majorAxis, ratio, startAngle, endAngle (radians)area + perimeter when full
HATCHHatchboundaries — pattern attributes live in properties

Unknown or future types still emit the full envelope with category: "Other" and all-null metrics — new entity types never change the envelope shape.

Glossary#

Line
A straight segment between two points.
Polyline
A connected sequence of points forming an open or closed shape; segments can bulge into arcs.
Arc
A portion of a circle, defined by a centre, radius, and start/end angles.
Circle
A full circle, defined by a centre and radius.
Ellipse
An oval, defined by a centre, major axis, and axis ratio.
Spline
A smooth curve through a set of control points (NURBS).
Text
Single-line or multi-line annotation, with its position and size.
Block insert
A placed reference to a reusable, named group of geometry (a “block”).

For complete example responses, see the Get result reference.

Example entity (line)
{
  "id": "1A4",
  "handle": "1A4",
  "type": "LINE",
  "category": "Geometry",
  "layer": "WALL",
  "layout": "M-2",
  "geometry": {
    "start": { "x": 0, "y": 0 },
    "end": { "x": 12000, "y": 0 }
  },
  "text": null,
  "reference": null,
  "properties": { "colorIndex": 7, "lineType": null, "lineweight": null, "visible": true, "solid": null, "patternName": null, "patternAngle": null, "patternScale": null },
  "bbox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 0 },
  "metrics": { "length": 12000, "area": null, "perimeter": null, "vertexCount": 2 }
}
Example entity (polyline)
{
  "id": "1B2",
  "handle": "1B2",
  "type": "LWPOLYLINE",
  "category": "Geometry",
  "layer": "WALL",
  "layout": "M-2",
  "geometry": {
    "vertices": [
      { "x": 0, "y": 0 },
      { "x": 12000, "y": 0 },
      { "x": 12000, "y": 7200 },
      { "x": 0, "y": 7200 }
    ],
    "closed": true,
    "filled": null
  },
  "text": null,
  "reference": null,
  "properties": { "colorIndex": 7, "lineType": null, "lineweight": null, "visible": true, "solid": null, "patternName": null, "patternAngle": null, "patternScale": null },
  "bbox": { "minX": 0, "minY": 0, "maxX": 12000, "maxY": 7200 },
  "metrics": { "length": 38400, "area": 86400000, "perimeter": 38400, "vertexCount": 4 }
}

Webhooks#

Skip polling. Pass webhookUrl when you call /v1/parse, or save an endpoint in the dashboard (up to two per account). CADLens then POSTs a JSON event as the job progresses and finishes. The request carries User-Agent: CADLens-Webhook/1.0 and a JSON body.

Events#

job.processing
The worker has started parsing the file.
job.completed
The result is ready — the event carries lightweight per-sheet metadata (entity/layer counts, bounding box, area, perimeter) and preview URLs, plus resultUrl to fetch the full parsed JSON geometry.
job.failed
The parse failed — the event carries the failure reason.

Delivery#

Your endpoint should respond 2xx within 10 seconds. A non-2xx (or timeout) is retried up to 3 times with exponential backoff (~2s, 4s, 8s, capped at 30s); after that the delivery is recorded as exhausted.

Verifying signatures#

Deliveries to a saved endpoint are signed. Each request carries X-CADLens-Signature: t=<ts>,v1=<hmac> and a X-CADLens-Timestamp header. Recompute the HMAC-SHA256 of <timestamp>.<raw request body> using your endpoint's signing secret (shown once when you create the endpoint) and compare it to the v1 value; reject requests whose timestamp is too old to stop replays.

i
As a belt-and-suspenders practice, on job.completed re-fetch GET /v1/jobs/{jobId}/result with your API key to read the authoritative result.
Event payload
{
  "eventId": "5f9c2b1e-7d3a-4e2f-9a1b-0c8d6e4f2a10",
  "sequence": 1,
  "event": "job.completed",
  "jobId": "job_lH9k2c",
  "status": "COMPLETED",
  "timestamp": "2026-05-21T10:42:19.000Z",
  "result": {
    "schemaVersion": "2.0.0",
    "imageUrl": "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-0.png?X-Amz-Expires=86400&...",
    "imageUrls": [
      "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-0.png?X-Amz-Expires=86400&...",
      "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-1.png?X-Amz-Expires=86400&...",
      "https://s3.amazonaws.com/cadlens-object/previews/job_lH9k2c/result/preview-2.png?X-Amz-Expires=86400&..."
    ],
    "metadata": { "filename": "floor-plan.dwg", "format": "DWG", "units": "mm" },
    "file": { "name": "floor-plan.dwg", "format": "DWG", "version": "AC1032", "units": "mm" },
    "summary": { "totalSheets": 3, "totalEntities": 1284, "totalLayers": 8, "truncated": false },
    "sheets": [
      /* Lightweight per-sheet metadata only — name, key, entityCount, layerCount,
         boundingBox, area, perimeter, imageUrl. No entities/geometry — fetch
         the full parsed result from resultUrl below. */
    ],
    "resultUrl": "https://api.cadlens.co/v1/jobs/job_lH9k2c/result"
  }
}
Receive the event
<?php
// Verify the signature, ack fast, then process.
$body   = file_get_contents('php://input');
$secret = getenv('CADLENS_WEBHOOK_SECRET'); // shown once when you create the endpoint

// Header: "X-CADLens-Signature: t=<ts>,v1=<hmac>"
$header = $_SERVER['HTTP_X_CADLENS_SIGNATURE'] ?? '';
parse_str(strtr($header, ',', '&'), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, $secret);

if (!hash_equals($expected, $sig['v1'] ?? '')) {
    http_response_code(400); // bad signature
    exit;
}

http_response_code(200); // ack fast — CADLens retries non-2xx up to 3 times

$event = json_decode($body, true);
if ($event['event'] === 'job.completed') {
    // Re-fetch GET /v1/jobs/{jobId}/result with your API key for the data.
    error_log('Parse done: ' . $event['jobId'] . ' ' . $event['status']);
}

Errors#

Errors return a flat JSON object with a human-readable error message and, sometimes, a programmatic code. Request-validation failures use a slightly richer shape — { "error": "Validation error", "details": { fieldErrors, formErrors } }. The HTTP status carries the category.

HTTP status codes#

CodeNameMeaning
200OKSuccessful request (or sync parse completed).
202AcceptedJob created/pending. Returned by `/v1/parse`.
400Bad RequestValidation error, unsupported/invalid file, or result not ready yet.
401UnauthorizedMissing, malformed, revoked, or expired API key.
403ForbiddenRequest not permitted (e.g. playground origin check).
404Not FoundJob does not exist for this key.
409ConflictState conflict, e.g. an API key that is already revoked.
412Precondition FailedA prerequisite is missing, e.g. `NO_ACTIVE_API_KEY`.
413Payload Too LargeFile exceeds your plan's upload limit (10 MB–250 MB depending on plan, 1 GB on Enterprise).
422UnprocessableFile was accepted but could not be parsed.
429Too Many RequestsMonthly plan quota reached.
500Internal ErrorUnexpected server error.
503Service UnavailableA dependency (e.g. Stripe webhooks) is not configured/available.

Common errors#

FieldTypeDescription
401
optional
`Invalid, revoked, or expired API key`
400
optional
`No file provided. Upload a file with field name "file".`
400
optional
`File header does not match a supported CAD format. Allowed: DWG/DXF/DWF/DGN/DWFX/PDF`
400
optional
`DGN V8 files are not supported. Only Bentley DGN V7 is currently accepted.`
400
optional
`Job result is not yet available` — the job is not COMPLETED yet.
413
optional
`File exceeds your plan's upload limit` — the per-upload cap depends on your plan (10 MB–250 MB, 1 GB on Enterprise).
429
optional
`Monthly limit of N requests reached for PLAN plan`
429
optional
DUPLICATE_FILE_WARNINGThe same filename was re-uploaded too quickly, or on a regular bot-like schedule. Response includes `attempt` and `maxAttempts`; the `maxAttempts`-th violation escalates to `DUPLICATE_FILE_BLOCKED`.
429
optional
DUPLICATE_FILE_BLOCKEDUploads of that filename are temporarily blocked after repeated violations. Response includes `retryAfterSeconds` and a `Retry-After` header; other filenames are unaffected.
400
optional
FILE_TOO_COMPLEXThe converted drawing is too large or intricate to process within the memory/size limit.
400
optional
CONVERSION_FAILEDThe underlying CAD-to-DXF conversion step failed or timed out.
400
optional
CORRUPT_FILEThe file could not be read or parsed — its contents are malformed or unreadable.
400
optional
EMPTY_DRAWINGThe drawing contains no extractable entities.
optional
PARSE_TIMEOUTParsing exceeded the server-side time limit — the job is marked FAILED; retry or contact support for very large drawings.
optional
FILE_REUSED_ACROSS_ACCOUNTSRisk signal (not a request-blocking error): the same file fingerprint was also uploaded from other accounts, contributing to abuse-risk scoring.
Error envelope
{
  "error": "File exceeds your plan's upload limit (25 MB)."
}
Catching errors
const r = await fetch(url, { headers: auth });

if (!r.ok) {
  const body = await r.json();
  console.error(
    `HTTP ${r.status}`,
    body.error,   // human-readable message
    body.code,    // optional programmatic code, e.g. "NO_ACTIVE_API_KEY"
  );
  return;
}

const job = await r.json();

Usage & quotas#

Usage is metered as a monthly quota per plan. Each successful parse counts as one request; failed parses and read calls (get job, result, image, list) do not count. The counter resets at the start of each calendar month (UTC).

FieldTypeDescription
Free
optional
50 / moTrial and hobby use.
Starter
optional
500 / moSmall projects.
Growth
optional
2,000 / moEarly production.
Pro
optional
10,000 / moHigher-volume apps.
Business
optional
40,000 / moHigh volume.
Enterprise
optional
UnlimitedNo monthly cap.

Once you hit your plan's limit, POST /v1/parse returns 429 Too Many Requests with a message like Monthly limit of N requests reached for PLAN plan until the next monthly reset or a plan upgrade.

Need more? Upgrade your plan in the dashboard, or email [email protected] for Enterprise.

Quota exceeded
{
  "error": "Monthly limit of 50 requests reached for FREE plan"
}

Changelog#

Notable changes to the API.

2026-07-17
fixed
3D solid drawings now render their hole and gland circles. Drawings containing ACIS 3DSOLID geometry previously dropped circular edges whose records carry a common attribute prefix — mounting-plate holes, cable-gland openings, and bolt circles were missing from PNG previews and the viewer, and mis-read arc records could inflate the drawing extents so content appeared tiny in a huge empty canvas. The ACIS record reader now locates geometry robustly regardless of record layout, extracting every solid edge (a reference drawing went from 388 dropped edges to zero). Results carry parserVersion 2.2.1; response shapes are unchanged and 2D drawings are unaffected.
2026-07-17
added
Solid-model drawings now render in 2D. DWG/DXF files containing ACIS 3DSOLID or BODY geometry previously showed nothing but text in model space. CADLens now extracts an edge wireframe directly from the solid's ACIS data — reading both the obfuscated SAT text form and the binary SAB form used by newer conversions — and injects it as ordinary line/polyline geometry, so solids now appear in the PNG preview, the 2D dashboard viewer, and as 3D lines in the 3D viewer. This is additive: it only affects drawings that contain solids, response shapes are unchanged, and 2D-only drawings behave exactly as before. Results carry parserVersion 2.2.0. Note: silhouette/hidden-line curves that some CAD tools regenerate for saved 2D views (e.g. some hole circles) may still be absent — see the 2.2.1 entry above for a related fix to dropped hole circles.
2026-07-16
fixed
Large drawings parse roughly 3× faster. Two internal optimizations for drawings whose converted geometry is very large (hundreds of MB to multi-GB): redundant re-conversion passes that could not change the outcome are skipped, and the streaming geometry filter fast-forwards over out-of-budget sections instead of scanning them line by line. Results are identical — same entities, layers, images, and truncated flag — only faster. Conversion time limits are also configurable server-side now, so very complex drawings that legitimately need several minutes to convert no longer fail early. No API shape changes.
2026-07-15
fixed
Sync mode no longer makes you wait for large files. POST /v1/parse with mode=sync now returns 202 immediately for uploads of 10 MB or more — with a message field explaining that parsing continues asynchronously (poll GET /v1/jobs/:jobId or use a webhook) — instead of holding the connection until the sync timeout. If a smaller drawing turns out to convert to very large geometry mid-parse, the sync wait is also released early with the same 202. Response shapes are unchanged; the 202 body and its optional message field already existed. Tip: prefer mode=async (the default) for large files.
2026-07-15
fixed
Very large drawings now parse instead of failing. Drawings whose converted geometry previously exceeded the processing limit (FILE_TOO_COMPLEX) are now processed through a constant-memory streaming filter: full layer/layout/metadata fidelity is kept, and geometry beyond the entity budget is cleanly dropped with truncated: true set in the result — the same flag already used when a drawing exceeds the 50,000-entity result cap. Only extreme conversions (over 10 GB of intermediate geometry) are still rejected with FILE_TOO_COMPLEX. No API shape changes; drawings that parsed before behave exactly as before.
2026-07-14
fixed
Large-file reliability: drawings whose converted geometry exceeds the processing limits now fail fast with a clear FAILED status and error message instead of remaining stuck in PROCESSING. Failure emails and job.failed webhooks now fire for every terminal outcome, including jobs interrupted by a server restart — an automatic recovery watchdog re-processes or fails any interrupted job, so no job can stay in PROCESSING indefinitely. No API shape changes: statuses, response fields, webhook and email payloads are unchanged.
2026-07-13
schema
Schema v2.0.0 — restructured entity envelope (breaking). Result responses (GET /v1/jobs/:id/result and sync POST /v1/parse) now carry top-level schemaVersion and parserVersion fields, a parseInfo section ({durationMs, warnings, errors} — durationMs is null for jobs parsed before this release), and summary.statistics with byType and byCategory entity counts. Breaking change to sheets[].entities: flat coordinate fields (start, end, vertices, center, position, radius, angles, scales) moved into a geometry object — read coordinates from entity.geometry now. Each entity also gains handle (the original CAD handle, or null — never derived from id), category (Geometry / Annotation / BlockReference / Hatch / Other), and always-present sibling objects: properties (colorIndex, lineType, lineweight, visible, plus HATCH pattern fields), bbox and metrics (computed helpers rounded to 6 decimals, null where not applicable), text (TEXT/MTEXT value/height/style) and reference (INSERT blockName). Original coordinate precision inside geometry is unchanged. Webhook payloads gain an additive result.schemaVersion field only. The viewer/render-entities endpoint is unaffected.
2026-07-12
fixed
Friendlier duplicate-upload protection on POST /v1/parse. Re-uploading a file with the same name is now only rejected when it arrives less than 10 seconds after the previous accepted upload (e.g. a double-click), or when uploads of that filename follow a regular bot-like schedule over the last 24 hours. Each rejection is a 429 with code DUPLICATE_FILE_WARNING and attempt/maxAttempts fields; after 5 violations that filename (only) is blocked for 5 minutes — the response carries code DUPLICATE_FILE_BLOCKED, retryAfterSeconds, and a Retry-After header. Waiting 10 seconds between re-uploads always succeeds, and uploads of other filenames are never affected.
2026-07-07
fixed
Viewer auto-fit and faster loads. Drawings containing stray far-away entities (orphaned block debris millions of units from the real content) no longer open as an invisible speck: the per-sheet bounding box in render data is now outlier-robust — when the raw extent dwarfs the drawing's validated header extent, entities outside 10× the 1st–99th percentile span are excluded from the fit box (all entities are still returned and rendered). Drawings whose only 3D content is unsupported solid types no longer report is3D with an empty 3D scene; a new additive metadata.unsupported3DCount field reports the count. Performance: job list responses no longer carry undocumented multi-MB result payloads (the documented Job shape is unchanged), the dashboard viewer downloads geometry once instead of twice (render data now bundles a meta object with is3D/units/format/filename), immutable result endpoints are HTTP-cached with ETags (reloads hit 304s), and expanded render entities are cached server-side for repeat opens.
2026-07-06
added
Background jobs, email notifications, and lighter webhooks. Large uploads keep processing server-side even if you close the page: dashboard uploads automatically email your account address when the job finishes (only if you left before it completed — live watchers are never emailed), and API callers can opt in by passing an optional notifyEmail form field to POST /v1/parse. The email deep-links to the job detail in your dashboard; if you're signed out you'll be asked to log in and land directly on the job. Webhook payloads are now lightweight: sheets[] carries per-sheet metadata only (name, key, entityCount, layerCount, boundingBox, area, perimeter, imageUrl) plus a new resultUrl field — fetch the full entities/layers geometry from GET /v1/jobs/:id/result, which is unchanged. This fixes webhook delivery timeouts on large drawings (multi-MB payloads previously exhausted retries). Also in this release: status polls may send watch=1 to signal a live viewer, parsing runs in a dedicated worker thread so the API stays responsive during heavy conversions, queued jobs/webhooks/emails process concurrently, and upload timeouts were raised for multi-MB files. API responses are now gzip-compressed when clients send Accept-Encoding (all HTTP clients do, automatically) — large results download ~12× faster with no client changes needed — and the dashboard viewer loads and pans large drawings much more smoothly (idle redraws eliminated, cached geometry, lightweight frames while panning).
2026-07-02
fixed
Viewer accuracy: entities drawn in a mirrored coordinate system (DXF extrusion normal 0,0,−1 — produced by AutoCAD MIRROR) now render at the correct position. Previously mirrored blocks (chairs, sofa cushions, fixtures) appeared far outside the drawing or at flipped positions. Nested block references under mirrored inserts also rotate correctly, and 2D entities with small Z-coordinate artifacts are no longer dropped from results.
2026-07-02
schema
HATCH patternLines: hatch entities in sheets[].entities now include the exact pattern-definition line families (angle, base point, offset, dash lengths — drawing units, rotation/scale applied). Viewers can render true hatch patterns (ANSI31–37, HONEY, STEEL, user patterns) instead of generic diagonals. patternAngle and patternScale are now reliably populated, and metadata.linetypePatterns / metadata.ltscale expose the LTYPE dash tables.
2026-06-30
added
Viewer optimization: the playground viewer resolves BYBLOCK/BYLAYER colors and linetypes through the full DXF inheritance chain, renders dashed/center/hidden linetypes from the drawing's LTYPE table, and ships per-sheet linetypePatterns and ltscale in render data. Entity batching, frustum culling, and level-of-detail keep large drawings interactive.
2026-06-27
added
Interactive viewer: full geometry rendering for complex DWG files. Block references (INSERT entities) are expanded server-side, text labels (TEXT/MTEXT) now appear on canvas, and paper-space layout tabs correctly project model-space content at viewport scale.
2026-06-25
added
sheet.key: each sheet in the sheets[] array now includes a key field — a unique HTML/CSS-safe slug derived from the display label (e.g. "floor-plan-2"). Deduplicated with a -2, -3 suffix when multiple sheets share the same name. Safe to use as an HTML id attribute.
2026-06-25
added
metadata.layoutLabels and metadata.layoutKeys added to result responses. layoutLabels contains the original sheet display names in image order (may repeat across sheets). layoutKeys contains the HTML/CSS-safe slugs, parallel to the existing layouts[] array.
2026-06-25
fixed
Per-viewport frozen-layer rendering: named layout sheets now correctly show only the layers that are visible in each viewport. Previously, all layers rendered on every sheet regardless of viewport freeze settings.
2026-06
added
Multi-script font support in PNG previews: Chinese (Simplified & Traditional), Japanese, Korean, Arabic, Hebrew, Thai, Cyrillic, Devanagari, and Latin text now render correctly in preview images. Previously only Latin characters were supported — non-Latin glyphs appeared as empty boxes. \U+XXXX Unicode escape sequences in DXF MTEXT entities are also decoded, preserving CJK labels embedded in older DXF files.
2026-06
schema
Result response restructured: entities and layers are now nested inside each sheets[] entry instead of being exposed as flat root-level vectorJson / layersJson arrays. Each sheet object contains name, index, imageUrl, entityCount, layerCount, boundingBox, area, perimeter, layers[], and entities[]. New top-level file and summary fields provide file metadata and aggregate counts. The root-level metadata and imageUrl/imageUrls fields are retained for backward compatibility.
2026-06
fixed
Auth loop: when dashboard localStorage was cleared while the frontend still held a valid JWT, the two apps redirected each other in an infinite loop. The login page now re-syncs the token to the dashboard via /auth/callback when the redirect origin is the dashboard.
2026-06
fixed
Sheet mapping: entities were incorrectly lumped into a single Model sheet when the parser omitted the layouts array from metadata. Entity layout tags are now used as the authoritative sheet grouping source.
2026-06
schema
imageUrls array added to result and image responses — one presigned S3 URL per layout sheet. Multi-sheet DWG/DXF/DWF files now produce a separate PNG preview per sheet.
2026-06
fixed
CAD preview rendering: DIMENSION lines and arrows, SOLID arrowheads, and ATTRIB attribute text (title block fields) now appear in rendered preview PNGs.
2026-05
schema
Result documents return typed entities, per-layer counts, drawing metadata with a boundingBox, and a signed PNG preview URL.
2026-05
added
Webhooks: job.processing, job.completed, and job.failed events, delivered to a per-parse webhookUrl or a saved endpoint.
2026-05
added
Format support: DWG, DXF, DWF, DWFx, Bentley DGN V7, and CAD-bearing PDF, validated by magic bytes up to 100 MB.
Base URL
https://api.cadlens.co/v1