ShipLock Docs
API reference for building apps with ShipLock's data store and deploying via the /shiplock skill.
Quickstart
Every app gets a built-in JSON data store and window.SL injected at serve time. To use them:
// window.SL is injected by ShipLock at serve time const SL = window.SL || { appId: '', api: 'https://api.shiplock.app', role: 'public', visitorToken: null } const ownerToken = localStorage.getItem('sl_token') // Auth headers — include whichever token is available const authHeaders = { 'Content-Type': 'application/json', ...(ownerToken ? { 'Authorization': 'Bearer ' + ownerToken } : {}), ...(SL.visitorToken ? { 'X-SL-Token': SL.visitorToken } : {}) } // Read data const res = await fetch(`${SL.api}/data/${SL.appId}`, { headers: authHeaders }) const { data, etag } = await res.json() // Write data (using etag for optimistic locking) await fetch(`${SL.api}/data/${SL.appId}`, { method: 'PUT', headers: { ...authHeaders, 'If-Match': etag }, body: JSON.stringify(newData) })
window.SL
ShipLock injects window.SL into the <head> of every served app at request time, after access control is resolved.
| Property | Type | Description |
|---|---|---|
| appId | string | The app's unique ID — use this in data API calls |
| api | string | Always https://api.shiplock.app |
| role | string | "owner", "member", or "public" |
| visitorToken | string | null | Short-lived HMAC token encoding role. Pass as X-SL-Token header. Valid for 4 hours. |
window.SL being undefined during local development: const SL = window.SL || {appId:'',api:'https://api.shiplock.app',role:'public',visitorToken:null}Data API
Each app has a single JSON store. Read it, replace it, or append to it.
| Method | Path | Description |
|---|---|---|
| GET | /data/:appId |
Returns {data, etag}. The etag is used for optimistic locking on PUT. |
| PUT | /data/:appId |
Replaces the entire data store. Pass If-Match: {etag} to avoid clobbering concurrent writes. Returns 412 if etag doesn't match. |
Append-only pattern
When your app's data policy is "Anyone can submit", use this CAS loop to safely append without clobbering concurrent submissions:
async function appendRow(newRow) { for (let i = 0; i < 3; i++) { const res = await fetch(`${SL.api}/data/${SL.appId}`, { headers: authHeaders }) const { data, etag } = await res.json() const next = [...(data || []), { ...newRow, _timestamp: new Date().toISOString() }] const put = await fetch(`${SL.api}/data/${SL.appId}`, { method: 'PUT', headers: { ...authHeaders, 'If-Match': etag }, body: JSON.stringify(next) }) if (put.ok) return if (put.status !== 412) throw new Error('Write failed') // 412 = concurrent write — retry with fresh etag } }
Data shapes
The data store accepts any valid JSON. For best compatibility with the dashboard data viewer and the /shiplock skill, use a flat array of objects:
// ✓ Recommended — flat array of objects [ { "name": "Alice", "status": "yes", "_timestamp": "2026-06-01T10:00:00Z" }, { "name": "Bob", "status": "no", "_timestamp": "2026-06-01T11:00:00Z" } ] // ✗ Avoid — nested objects are hard to display and edit { "users": { "alice": { "status": "yes" } } }
The dashboard data viewer normalizes any shape to rows, but nested objects will display as JSON.stringify(…) strings.
Role matrix
What a caller can do depends on their role and the app's data policy:
| Caller | Policy: View only | Policy: Anyone can submit | Policy: Anyone can edit |
|---|---|---|---|
| Owner (sl_token) | GET + PUT | GET + PUT | GET + PUT |
| Member (visitorToken) | GET + PUT | GET + PUT | GET + PUT |
| Public (no token) | GET only | GET + PUT (append enforced) | GET + PUT |
Private and group apps return 403 for unauthenticated data requests regardless of data policy.
Auth endpoints
Base URL: https://api.shiplock.app
| Method | Path | Description |
|---|---|---|
| POST | /auth/session | Login. Body: {email, password}. Returns {token, userId, email, plan, subdomain} and sets sl_session cookie. |
| DELETE | /auth/session | Logout. Clears the session cookie. |
| GET | /auth/profile | Returns {email, name, plan, subdomain}. Requires Authorization: Bearer {token}. |
| PATCH | /auth/profile | Update display name. Body: {name}. |
| POST | /auth/password-reset | Send password reset email. Body: {email}. |
| POST | /auth/change-password | Body: {currentPassword, newPassword}. |
Apps endpoints
| Method | Path | Description |
|---|---|---|
| GET | /apps | List all apps. Returns {apps: [{id, name, slug, policy, subdomain, dataPolicy, viewCount, views7d, expiresAt, …}]} |
| GET | /apps/:id | Single app record. |
| PATCH | /apps/:id | Update name, policy, slug, or dataPolicy. |
| DELETE | /apps/:id | Mark app as dormant. |
| GET | /apps/:id/source | Raw HTML source. |
| GET | /apps/:id/versions | Version history. Returns {versions: [{version, deployedAt, sizeBytes, isCurrent}]} |
| POST | /apps/:id/rollback | Body: {version: number}. Rolls back to that version. |
Deploy endpoint
Deploy or update an app. Uses multipart form data.
const fd = new FormData() fd.append('name', 'My App') fd.append('policy', 'lnk') // pub | lnk | grp | prv fd.append('dataPolicy', 'append-only') // read-only | append-only | read-write fd.append('file', htmlBlob, 'app.html') // fd.append('appId', existingId) // omit to create new, include to redeploy const res = await fetch('https://api.shiplock.app/deploy', { method: 'POST', headers: { 'Authorization': 'Bearer ' + ownerToken }, body: fd }) const { appId, url, version } = await res.json() // url → 'your-subdomain.shiplock.app/my-app'
MCP tools
The MCP server is available at https://api.shiplock.app/mcp. Authenticate by passing your token as a URL parameter: ?token=YOUR_TOKEN.
| Tool | Description |
|---|---|
list_apps | List your deployed apps. Returns name, URL, policy, and app ID. |
deploy | Deploy an HTML string as an app. Params: html (required), name, policy (pub|lnk|grp|prv), dataPolicy, appId (to redeploy). |
start_trial | Create a new ShipLock account. Returns a session token. Used by the /shiplock skill for first-time users. |
MCP connector URLs
Claude Code — add to your MCP settings:
{
"mcpServers": {
"shiplock": {
"url": "https://api.shiplock.app/mcp",
"headers": { "Authorization": "Bearer YOUR_TOKEN" }
}
}
}
Claude.ai — use this URL as an MCP connector:
https://api.shiplock.app/mcp?token=YOUR_TOKEN
The /shiplock skill
Install the /shiplock skill in Claude Code by running this in your terminal:
curl -s https://shiplock.app/skill.md > ~/.claude/commands/shiplock.md
This adds /shiplock as a global command. Type /shiplock in any Claude Code project to build and deploy. See the skill landing page for examples and a feature overview.