Developer platform
SpecLatch Read API
Read-only, versioned, tier-scoped. The same live data behind your portal and PDFs — served as JSON.
Overview
The SpecLatch Read API is a read-only, versioned HTTP API over your organization's tables and records — the same live data that powers your public portal and generated PDFs. Point your website, dealer portal, or quoting tool at it and retire the exported copy.
Every path lives under https://app.speclatch.com/api/v1 and returns JSON (downloads return the file itself). Breaking changes only ever arrive in a new version prefix — integrations against /api/v1 keep working.
The API sends CORS headers, so browser-side calls work — but a key embedded in client code is visible to anyone who opens the page. For public websites, call the API from your server or edge layer and keep the key a secret.
curl "https://app.speclatch.com/api/v1/tables" \
-H "Authorization: Bearer $SPECLATCH_API_KEY"A first call: list the tables your key can see.
Authentication
Every request carries an organization-scoped API key as a bearer token. Keys start with slk_ and are shown once at creation — store them like any other secret. The Read API comes with the Growth plan and up (pricing); how many keys a workspace can mint follows its plan.
Each key has a visibility scope — public, internal, or confidential — and that scope is the most the key can ever see. A PUBLIC-scoped key receives only published records and public fields and documents: issue one of those for anything customer-facing, and keep fuller-scoped keys for internal tooling.
Workspace admins create and revoke keys under Settings → API keys. Revocation is immediate.
curl "https://app.speclatch.com/api/v1/records" \
-H "Authorization: Bearer slk_your_key_here"List tables
GET/api/v1/tables
Every table visible to the key, with its published-record count — the natural first call for discovering an organization's catalog structure.
Request
curl "https://app.speclatch.com/api/v1/tables" \
-H "Authorization: Bearer $SPECLATCH_API_KEY"// Node 18+ (or the browser — CORS is enabled, but keep keys server-side)
const res = await fetch("https://app.speclatch.com/api/v1/tables", {
headers: { Authorization: `Bearer ${process.env.SPECLATCH_API_KEY}` },
});
if (!res.ok) throw new Error(`SpecLatch API ${res.status}`);
const { tables } = await res.json();import os
import requests
res = requests.get(
"https://app.speclatch.com/api/v1/tables",
headers={"Authorization": f"Bearer {os.environ['SPECLATCH_API_KEY']}"},
)
res.raise_for_status()
tables = res.json()["tables"]Response · 200
{
"tables": [
{
"id": "tbl_ac1m0t",
"name": "AC Induction Motors",
"description": "NEMA frame motors, 1-20 HP.",
"publishedRecordCount": 4
},
{
"id": "tbl_gearbx",
"name": "Inline Gear Reducers",
"description": null,
"publishedRecordCount": 9
}
]
}- Tables are ordered by their configured sort order, then by name.
descriptionisnullwhen the table has none.
List records
GET/api/v1/records
Records visible to the key's tier, with every visible spec value — filter to one table with tableId, or search with q.
| Parameter | In | Type | Description |
|---|---|---|---|
| tableId | query | string | Limit results to one table (an id from /tables). |
| q | query | string | Full-text search over names, model numbers and spec values. Returns at most 100 matches. |
| lang | query | string — en | fr | es | Display language for labels, choice values and prose, where the organization has translated them. Stable field keys, ids and units are unaffected. |
Request
curl "https://app.speclatch.com/api/v1/records?tableId=tbl_ac1m0t" \
-H "Authorization: Bearer $SPECLATCH_API_KEY"// Node 18+ (or the browser — CORS is enabled, but keep keys server-side)
const res = await fetch("https://app.speclatch.com/api/v1/records?tableId=tbl_ac1m0t", {
headers: { Authorization: `Bearer ${process.env.SPECLATCH_API_KEY}` },
});
if (!res.ok) throw new Error(`SpecLatch API ${res.status}`);
const { records } = await res.json();import os
import requests
res = requests.get(
"https://app.speclatch.com/api/v1/records",
params={"tableId": "tbl_ac1m0t"},
headers={"Authorization": f"Bearer {os.environ['SPECLATCH_API_KEY']}"},
)
res.raise_for_status()
records = res.json()["records"]Response · 200
{
"records": [
{
"id": "rec_8f2k31",
"name": "Example Motor 20",
"modelNumber": "EXM20",
"sku": "EXM20-230-4P",
"description": "Foot-mounted AC induction motor, 20 HP, 230/460 V.",
"status": "PUBLISHED",
"updatedAt": "2026-08-14T18:22:09.000Z",
"specs": [
{
"field": "rated_power",
"name": "Rated Power",
"type": "UNIT_NUMBER",
"value": 20,
"unit": "HP",
"dual": {
"imperial": { "value": 20, "unit": "HP" },
"metric": { "value": 14914, "unit": "W" }
}
},
{
"field": "compatible_accessories",
"name": "Compatible Accessories",
"type": "LINK",
"value": [
{ "id": "rec_9b7q44", "name": "Mounting Base", "modelNumber": "ACC-BASE" }
]
},
{
"field": "speed_range",
"name": "Speed Range",
"type": "ROLLUP",
"value": [850, 1725],
"unit": "RPM"
}
],
"images": [
{
"id": "att_1",
"filename": "front.png",
"mimeType": "image/png",
"sizeBytes": 20481,
"isPrimary": true,
"downloadUrl": "/api/v1/attachments/att_1"
}
],
"files": [
{
"id": "att_2",
"filename": "outline-drawing.pdf",
"mimeType": "application/pdf",
"sizeBytes": 88213,
"downloadUrl": "/api/v1/attachments/att_2"
}
]
}
]
}- Results are ordered by name and unpaginated: a request returns every record visible to the key. Use
tableIdandqto narrow. - Responses served in French or Spanish include a top-level
langkey echoing the language.
Get a record
GET/api/v1/records/{id}
One record in full: tier-projected specs, images, files — plus documents, the generated PDFs (spec sheets, catalogs) visible at the key's tier.
| Parameter | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The record id (from /records). |
| lang | query | string — en | fr | es | Display language for labels, choice values and prose. |
Request
curl "https://app.speclatch.com/api/v1/records/rec_8f2k31" \
-H "Authorization: Bearer $SPECLATCH_API_KEY"// Node 18+ (or the browser — CORS is enabled, but keep keys server-side)
const res = await fetch("https://app.speclatch.com/api/v1/records/rec_8f2k31", {
headers: { Authorization: `Bearer ${process.env.SPECLATCH_API_KEY}` },
});
if (!res.ok) throw new Error(`SpecLatch API ${res.status}`);
const { record } = await res.json();import os
import requests
res = requests.get(
"https://app.speclatch.com/api/v1/records/rec_8f2k31",
headers={"Authorization": f"Bearer {os.environ['SPECLATCH_API_KEY']}"},
)
res.raise_for_status()
record = res.json()["record"]Response · 200
{
"record": {
"id": "rec_8f2k31",
"name": "Example Motor 20",
"modelNumber": "EXM20",
"sku": "EXM20-230-4P",
"description": "Foot-mounted AC induction motor, 20 HP, 230/460 V.",
"status": "PUBLISHED",
"updatedAt": "2026-08-14T18:22:09.000Z",
"specs": [
{
"field": "rated_power",
"name": "Rated Power",
"type": "UNIT_NUMBER",
"value": 20,
"unit": "HP",
"dual": {
"imperial": { "value": 20, "unit": "HP" },
"metric": { "value": 14914, "unit": "W" }
}
},
{
"field": "compatible_accessories",
"name": "Compatible Accessories",
"type": "LINK",
"value": [
{ "id": "rec_9b7q44", "name": "Mounting Base", "modelNumber": "ACC-BASE" }
]
},
{
"field": "speed_range",
"name": "Speed Range",
"type": "ROLLUP",
"value": [850, 1725],
"unit": "RPM"
}
],
"images": [
{
"id": "att_1",
"filename": "front.png",
"mimeType": "image/png",
"sizeBytes": 20481,
"isPrimary": true,
"downloadUrl": "/api/v1/attachments/att_1"
}
],
"files": [
{
"id": "att_2",
"filename": "outline-drawing.pdf",
"mimeType": "application/pdf",
"sizeBytes": 88213,
"downloadUrl": "/api/v1/attachments/att_2"
}
]
,
"documents": [
{
"id": "doc_5t1m20",
"title": "EXM20 Spec Sheet",
"filename": "exm20-spec-sheet.pdf",
"docType": "SPEC_SHEET",
"mimeType": "application/pdf",
"sizeBytes": 182340,
"downloadUrl": "/api/v1/documents/doc_5t1m20/download"
}
]
}
}The detail payload wraps the record in a record key and adds documents.
- A 404 covers both records that don't exist and records the key's tier may not see — the API never confirms the existence of hidden data.
- Responses served in French or Spanish include a top-level
langkey.
Download an attachment
GET/api/v1/attachments/{id}
The raw bytes of an image or file from a record's images / files lists — exactly what each entry's downloadUrl points at.
| Parameter | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The attachment id (from an images or files entry). |
Request
curl "https://app.speclatch.com/api/v1/attachments/att_2" \
-H "Authorization: Bearer $SPECLATCH_API_KEY" \
-o outline-drawing.pdf// Node 18+ (or the browser — CORS is enabled, but keep keys server-side)
const res = await fetch("https://app.speclatch.com/api/v1/attachments/att_2", {
headers: { Authorization: `Bearer ${process.env.SPECLATCH_API_KEY}` },
});
if (!res.ok) throw new Error(`SpecLatch API ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());import os
import requests
res = requests.get(
"https://app.speclatch.com/api/v1/attachments/att_2",
headers={"Authorization": f"Bearer {os.environ['SPECLATCH_API_KEY']}"},
)
res.raise_for_status()
with open("outline-drawing.pdf", "wb") as f:
f.write(res.content)Response · 200
{
"_comment": "Binary response - the file bytes, not JSON.",
"Content-Type": "application/pdf",
"Content-Length": "88213",
"Content-Disposition": "attachment; filename=\"outline-drawing.pdf\""
}The response is the file itself; headers shown for orientation. Images are served inline, other types as downloads.
- 404s unless the attachment's field is at or below the key's tier AND the record itself is visible.
Download a document
GET/api/v1/documents/{id}/download
The raw bytes of a generated document (spec sheet, catalog) from a record's documents list.
| Parameter | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The document id (from a record's documents list). |
Request
curl "https://app.speclatch.com/api/v1/documents/doc_5t1m20/download" \
-H "Authorization: Bearer $SPECLATCH_API_KEY" \
-o exm20-spec-sheet.pdf// Node 18+ (or the browser — CORS is enabled, but keep keys server-side)
const res = await fetch("https://app.speclatch.com/api/v1/documents/doc_5t1m20/download", {
headers: { Authorization: `Bearer ${process.env.SPECLATCH_API_KEY}` },
});
if (!res.ok) throw new Error(`SpecLatch API ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());import os
import requests
res = requests.get(
"https://app.speclatch.com/api/v1/documents/doc_5t1m20/download",
headers={"Authorization": f"Bearer {os.environ['SPECLATCH_API_KEY']}"},
)
res.raise_for_status()
with open("exm20-spec-sheet.pdf", "wb") as f:
f.write(res.content)Response · 200
{
"_comment": "Binary response - the document bytes, not JSON.",
"Content-Type": "application/pdf",
"Content-Length": "182340"
}The response is the document itself; headers shown for orientation.
- 404s unless the document is flagged visible at the key's tier and its record is visible.
Response shape
Spec values are unit-aware: numeric fields carry their unit and a dual imperial/metric rendering, keyed by stable machine-readable field keys. Records also carry sku, description, status and updatedAt — poll updatedAt to know when something changed.
LINK specs list the records they point at; LOOKUP and ROLLUP specs carry values derived from those linked records. A range-style rollup returns value as a [min, max] pair with its unit (and no dual). A linked record is only ever named when your key may see that record too — a PUBLIC key never learns about an unpublished one.
Add ?lang=fr or ?lang=es to get display strings (name, choice values, descriptions) in that language where the organization has translated them; the response then carries a top-level lang key. Languages beyond the workspace's default follow its plan. The stable field keys, ids and units never change with the language, so your integration code doesn't either.
{
"field": "rated_torque",
"name": "Rated Torque",
"type": "UNIT_NUMBER",
"value": 240,
"unit": "N·m",
"dual": {
"imperial": { "value": 177.01, "unit": "lb·ft" },
"metric": { "value": 240, "unit": "N·m" }
}
}The anatomy of one spec entry. (All sample data in this reference is fake.)
Errors
Every error is JSON with a machine-readable code and a human-readable message. The codes are UNAUTHORIZED (401), NOT_FOUND (404), RATE_LIMITED (429) and INTERNAL (500) — an unexpected server error still returns the same envelope.
A 404 covers missing resources AND resources your key's tier may not see — the API deliberately doesn't distinguish the two, so hidden data is never confirmed to exist.
{
"error": {
"code": "NOT_FOUND",
"message": "Record not found"
}
}Rate limits
Limits apply per key and follow the workspace's plan: 120 requests per minute on Growth, 600 on Professional and above. Every response — success or error — includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a unix timestamp).
Exceeding the limit returns 429 with a Retry-After header. Back off until the window resets; a small response cache on your side goes a long way for catalog data that changes a few times a day.
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
X-RateLimit-Reset: 1767139200Versioning
The version lives in the path: /api/v1. Additive changes — new fields, new endpoints, new query parameters — ship in place; anything that would break an existing integration arrives in a new version prefix instead.
Within v1 so far: linked-record, lookup and rollup specs plus images/files (1.1); ?lang= display languages (1.2); the tables/records vocabulary (1.3); full machine-readable schemas in the OpenAPI description (1.4).
OpenAPI spec
The machine-readable description of this API lives at https://www.speclatch.com/openapi.yaml — an OpenAPI 3.1 document with typed schemas for every payload, ready for client generation and request validation tooling.
The spec and this reference are kept in lockstep by automated checks: an endpoint can't ship, change shape, or gain a parameter without both being updated together.