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.

PropertyTypeDescription
appIdstringThe app's unique ID — use this in data API calls
apistringAlways https://api.shiplock.app
rolestring"owner", "member", or "public"
visitorTokenstring | nullShort-lived HMAC token encoding role. Pass as X-SL-Token header. Valid for 4 hours.
Always guard against 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.

MethodPathDescription
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:

CallerPolicy: View onlyPolicy: Anyone can submitPolicy: Anyone can edit
Owner (sl_token)GET + PUTGET + PUTGET + PUT
Member (visitorToken)GET + PUTGET + PUTGET + PUT
Public (no token)GET onlyGET + 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

MethodPathDescription
POST/auth/sessionLogin. Body: {email, password}. Returns {token, userId, email, plan, subdomain} and sets sl_session cookie.
DELETE/auth/sessionLogout. Clears the session cookie.
GET/auth/profileReturns {email, name, plan, subdomain}. Requires Authorization: Bearer {token}.
PATCH/auth/profileUpdate display name. Body: {name}.
POST/auth/password-resetSend password reset email. Body: {email}.
POST/auth/change-passwordBody: {currentPassword, newPassword}.

Apps endpoints

MethodPathDescription
GET/appsList all apps. Returns {apps: [{id, name, slug, policy, subdomain, dataPolicy, viewCount, views7d, expiresAt, …}]}
GET/apps/:idSingle app record.
PATCH/apps/:idUpdate name, policy, slug, or dataPolicy.
DELETE/apps/:idMark app as dormant.
GET/apps/:id/sourceRaw HTML source.
GET/apps/:id/versionsVersion history. Returns {versions: [{version, deployedAt, sizeBytes, isCurrent}]}
POST/apps/:id/rollbackBody: {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.

ToolDescription
list_appsList your deployed apps. Returns name, URL, policy, and app ID.
deployDeploy an HTML string as an app. Params: html (required), name, policy (pub|lnk|grp|prv), dataPolicy, appId (to redeploy).
start_trialCreate 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.