damnlinesdamnlines.com

damnlines API.

Live wait times, snapshots, and people counts for NYC venues for your projects

https://api.damnlines.com/v1
//00 — Quick Start

Every request needs a bearer token in the Authorization header. Keys look like dl_live_<id>.<secret>. The secret half is shown once at creation — store it somewhere safe.

Authorization: Bearer dl_live_xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
Create an API key →

Free, requires an account. Free tier: 60 requests/min, 5,000 requests/day per key.

//01 — Locations

Locations describe a venue we monitor — its slug, display name, address, hours, and current open / count / wait status if the camera is live.

Finding a location slug: every endpoint that takes location or {slug} wants the API slug returned by GET /v1/locations (e.g. salt_hanks). It's not always the same as the URL slug on damnlines.com, so always source it from this endpoint rather than scraping the site.

GET/v1/locations
Auth: Required

List every location the API exposes, with current open/closed status and queue counts.

curl https://api.damnlines.com/v1/locations \
  -H "Authorization: Bearer dl_live_xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
Example response
{
  "data": [
    {
      "slug": "salt_hanks",
      "display_name": "Salt Hank's",
      "address": "West Village, NY 10014",
      "geo": {
        "lat": 40.7318,
        "lng": -74.0036
      },
      "timezone": "America/New_York",
      "hours": {
        "mon": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "tue": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "wed": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "thu": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "fri": [
          {
            "open": "11:00",
            "close": "23:00"
          }
        ],
        "sat": [
          {
            "open": "10:00",
            "close": "23:00"
          }
        ],
        "sun": [
          {
            "open": "10:00",
            "close": "21:00"
          }
        ]
      },
      "status": {
        "is_open": true,
        "current_count": 12,
        "wait_minutes": 18,
        "captured_at": "2024-04-29T17:11:55Z"
      },
      "stream_supported": true
    },
    {
      "slug": "johns_on_bleecker",
      "display_name": "John's of Bleecker Street",
      "address": "West Village, NY 10014",
      "geo": {
        "lat": 40.7316,
        "lng": -74.0034
      },
      "timezone": "America/New_York",
      "hours": {
        "mon": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "tue": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "wed": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "thu": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "fri": [
          {
            "open": "11:00",
            "close": "23:00"
          }
        ],
        "sat": [
          {
            "open": "10:00",
            "close": "23:00"
          }
        ],
        "sun": [
          {
            "open": "10:00",
            "close": "21:00"
          }
        ]
      },
      "status": {
        "is_open": true,
        "current_count": 4,
        "wait_minutes": 6,
        "captured_at": "2024-04-29T17:11:56Z"
      },
      "stream_supported": true
    },
    {
      "slug": "prince_st",
      "display_name": "Prince Street Pizza",
      "address": "SoHo, NY 10012",
      "geo": {
        "lat": 40.7232,
        "lng": -73.9947
      },
      "timezone": "America/New_York",
      "hours": {
        "mon": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "tue": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "wed": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "thu": [
          {
            "open": "11:00",
            "close": "22:00"
          }
        ],
        "fri": [
          {
            "open": "11:00",
            "close": "23:00"
          }
        ],
        "sat": [
          {
            "open": "10:00",
            "close": "23:00"
          }
        ],
        "sun": [
          {
            "open": "10:00",
            "close": "21:00"
          }
        ]
      },
      "status": {
        "is_open": false,
        "current_count": null,
        "wait_minutes": null,
        "captured_at": null
      },
      "stream_supported": true
    }
  ]
}

Cacheable. Responses include Cache-Control: public, max-age=300, s-maxage=600 and an ETag. status.current_count and status.wait_minutes are null when the camera is outside operating hours.

GET/v1/locations/{slug}
Auth: Required

Fetch a single location by slug.

Path parameters
NameTypeRequiredDescription
slugstringyesThe location slug, e.g. salt_hanks. Get the full list from GET /v1/locations.
curl https://api.damnlines.com/v1/locations/salt_hanks \
  -H "Authorization: Bearer dl_live_xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
Example response
{
  "data": {
    "slug": "salt_hanks",
    "display_name": "Salt Hank's",
    "address": "West Village, NY 10014",
    "geo": {
      "lat": 40.7318,
      "lng": -74.0036
    },
    "timezone": "America/New_York",
    "hours": {
      "mon": [
        {
          "open": "11:00",
          "close": "22:00"
        }
      ],
      "tue": [
        {
          "open": "11:00",
          "close": "22:00"
        }
      ],
      "wed": [
        {
          "open": "11:00",
          "close": "22:00"
        }
      ],
      "thu": [
        {
          "open": "11:00",
          "close": "22:00"
        }
      ],
      "fri": [
        {
          "open": "11:00",
          "close": "23:00"
        }
      ],
      "sat": [
        {
          "open": "10:00",
          "close": "23:00"
        }
      ],
      "sun": [
        {
          "open": "10:00",
          "close": "21:00"
        }
      ]
    },
    "status": {
      "is_open": true,
      "current_count": 12,
      "wait_minutes": 18,
      "captured_at": "2024-04-29T17:11:55Z"
    },
    "stream_supported": true
  }
}
//02 — Snapshots

Point-in-time still frames with a people count and wait estimate. Use /v1/snapshots/latest for the freshest one; /v1/snapshots for paginated backfill.

GET/v1/snapshots/latest
Auth: Required

Fetch the most recent snapshot for a location.

Query parameters
NameTypeRequiredDescription
locationstringyesLocation slug.
curl "https://api.damnlines.com/v1/snapshots/latest?location=salt_hanks" \
  -H "Authorization: Bearer dl_live_xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
Example response
{
  "data": {
    "id": "snap_aaaaddzk253xq",
    "location": "salt_hanks",
    "captured_at": "2024-04-29T17:11:55Z",
    "image_url": "https://cdn.damnlines.com/sample.jpg",
    "image_url_valid_for_seconds": 86400,
    "people_count": 12,
    "wait_minutes": 18,
    "pipeline_version": 2
  }
}

Sent with Cache-Control: no-store. image_url is guaranteed valid for at least image_url_valid_for_seconds (24 hours from captured_at on the free tier). It may continue to work after that, but no contract.

GET/v1/snapshots
Auth: Required

Backfill historical snapshots for a location, paginated by encrypted cursor.

Query parameters
NameTypeRequiredDescription
locationstringyesLocation slug.
sinceISO 8601 datetimeyesInclusive lower bound. Must include a timezone offset (Z or ±HH:MM). Naive timestamps are rejected with 400.
untilISO 8601 datetimeyesExclusive upper bound. (until - since) must be ≤ max_window_days for your tier (free: 30).
limitintegernoMax items per page. Default 100, max 1000.
cursorstringnoOpaque pagination cursor returned by the previous response. Cursors are HMAC-protected and valid for 24 hours.
curl "https://api.damnlines.com/v1/snapshots?location=salt_hanks&since=2026-04-22T00:00:00Z&until=2026-04-29T00:00:00Z&limit=100" \
  -H "Authorization: Bearer dl_live_xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
Example response
{
  "data": [
    {
      "id": "snap_aaaaddzkzslfa",
      "location": "salt_hanks",
      "captured_at": "2024-04-29T17:00:02Z",
      "image_url": "https://cdn.damnlines.com/snap-2.jpg",
      "image_url_valid_for_seconds": 86400,
      "people_count": 7,
      "wait_minutes": 12,
      "pipeline_version": 2
    },
    {
      "id": "snap_aaaaddzkzsjgq",
      "location": "salt_hanks",
      "captured_at": "2024-04-29T17:00:01Z",
      "image_url": "https://cdn.damnlines.com/snap-1.jpg",
      "image_url_valid_for_seconds": 86400,
      "people_count": 6,
      "wait_minutes": 11,
      "pipeline_version": 2
    },
    {
      "id": "snap_aaaaddzkzshia",
      "location": "salt_hanks",
      "captured_at": "2024-04-29T17:00:00Z",
      "image_url": "https://cdn.damnlines.com/snap-0.jpg",
      "image_url_valid_for_seconds": 86400,
      "people_count": 5,
      "wait_minutes": 10,
      "pipeline_version": 2
    }
  ],
  "pagination": {
    "next_cursor": null,
    "has_more": false
  }
}

Backfill windows over your tier's max_window_days return 400 with code window_too_large. Use the cursor to walk pages — never construct one by hand. Results are returned newest-first.

//03 — Videos

Short clip recordings — same backfill model as snapshots, plus duration and thumbnail.

GET/v1/videos
Auth: Required

Backfill short clip recordings, with thumbnails and per-clip people counts.

Query parameters
NameTypeRequiredDescription
locationstringyesLocation slug.
sinceISO 8601 datetimeyesInclusive lower bound.
untilISO 8601 datetimeyesExclusive upper bound.
limitintegernoMax items per page. Default 100, max 1000.
cursorstringnoPagination cursor from the previous response.
curl "https://api.damnlines.com/v1/videos?location=salt_hanks&since=2026-04-22T00:00:00Z&until=2026-04-29T00:00:00Z" \
  -H "Authorization: Bearer dl_live_xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
Example response
{
  "data": [
    {
      "id": "vid_aaaaddzkzsjgq",
      "location": "salt_hanks",
      "captured_at": "2024-04-29T17:00:01Z",
      "video_url": "https://cdn.damnlines.com/videos/salt_hanks/hanks_01_20240429170001.mp4",
      "thumbnail_url": "https://cdn.damnlines.com/frames/2/thumb-2.jpg",
      "duration_seconds": null,
      "image_url_valid_for_seconds": 86400,
      "people_count": 6,
      "wait_minutes": 11,
      "pipeline_version": 2
    },
    {
      "id": "vid_aaaaddzkzshia",
      "location": "salt_hanks",
      "captured_at": "2024-04-29T17:00:00Z",
      "video_url": "https://cdn.damnlines.com/videos/salt_hanks/hanks_01_20240429170000.mp4",
      "thumbnail_url": "https://cdn.damnlines.com/frames/1/thumb-1.jpg",
      "duration_seconds": null,
      "image_url_valid_for_seconds": 86400,
      "people_count": 5,
      "wait_minutes": 10,
      "pipeline_version": 2
    }
  ],
  "pagination": {
    "next_cursor": null,
    "has_more": false
  }
}

Same pagination model as /v1/snapshots. duration_seconds and thumbnail_url may be null for older clips.

//04 — Line Counts

Line-count timeseries. Use interval=raw for every detection event, or 1m / 5m / 1h for bucketed stats.

GET/v1/lines
Auth: Required

Line-count timeseries — raw detection events or aggregated buckets.

Query parameters
NameTypeRequiredDescription
locationstringyesLocation slug.
sinceISO 8601 datetimeyesInclusive lower bound.
untilISO 8601 datetimeyesExclusive upper bound.
intervalraw | 1m | 5m | 1hnoDefault raw. raw returns every detection event in data_raw. 1m / 5m / 1h return mean/min/max per bucket in data_aggregated.
limitintegernoMax items per page. Default 100, max 1000.
cursorstringnoPagination cursor from the previous response.
curl "https://api.damnlines.com/v1/lines?location=salt_hanks&since=2026-04-29T00:00:00Z&until=2026-04-29T01:00:00Z&interval=5m" \
  -H "Authorization: Bearer dl_live_xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
Example response (interval=raw)
{
  "interval": "raw",
  "data_raw": [
    {
      "captured_at": "2024-04-29T17:00:02Z",
      "location": "salt_hanks",
      "people_count": 7,
      "wait_minutes": 12,
      "pipeline_version": 2
    },
    {
      "captured_at": "2024-04-29T17:00:01Z",
      "location": "salt_hanks",
      "people_count": 6,
      "wait_minutes": 11,
      "pipeline_version": 2
    },
    {
      "captured_at": "2024-04-29T17:00:00Z",
      "location": "salt_hanks",
      "people_count": 5,
      "wait_minutes": 10,
      "pipeline_version": 2
    }
  ],
  "data_aggregated": null,
  "pagination": {
    "next_cursor": null,
    "has_more": false
  }
}
Example response (interval=5m)
{
  "interval": "5m",
  "data_raw": null,
  "data_aggregated": [
    {
      "bucket_start": "2024-04-29T17:00:00Z",
      "bucket_seconds": 300,
      "location": "salt_hanks",
      "count_samples": 5,
      "people_mean": 12,
      "people_min": 10,
      "people_max": 14,
      "wait_minutes_mean": 17
    }
  ],
  "pagination": {
    "next_cursor": null,
    "has_more": false
  }
}

When interval=raw, data_raw is a list of detection events and data_aggregated is null. For 1m / 5m / 1h, data_aggregated carries the bucket stats and data_raw is null. The two arrays are mutually exclusive.

//05 — Streaming

Live events fan out the moment a new upload is processed. Two transports, identical payloads — SSE at /v1/stream (recommended) or WebSocket at /v1/stream/ws.

GET/v1/stream
Auth: Required

Server-Sent Events. Subscribe to snapshots, line counts, and clips as they're produced.

Query parameters
NameTypeRequiredDescription
locationstring (comma-separated)noFilter to one or more location slugs, e.g. location=salt_hanks,prince_st. Defaults to all locations your scope allows.
eventsstring (comma-separated)noFilter event types: snapshot,line_count,video. Defaults to all.
curl -N -H "Accept: text/event-stream" \
  -H "Authorization: Bearer dl_live_xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" \
  "https://api.damnlines.com/v1/stream?location=salt_hanks"
Example event (snapshot)
{
  "id": "1714410715123-0001",
  "event": "snapshot",
  "location": "salt_hanks",
  "captured_at": "2024-04-29T17:11:55Z",
  "data": {
    "id": "snap_aaaaddzk253xq",
    "location": "salt_hanks",
    "captured_at": "2024-04-29T17:11:55Z",
    "image_url": "https://cdn.damnlines.com/sample.jpg",
    "image_url_valid_for_seconds": 86400,
    "people_count": 12,
    "wait_minutes": 18,
    "pipeline_version": 2
  }
}
Example event (line_count)
{
  "id": "1714410715123-0001",
  "event": "line_count",
  "location": "salt_hanks",
  "captured_at": "2024-04-29T17:11:55Z",
  "data": {
    "captured_at": "2024-04-29T17:11:55Z",
    "location": "salt_hanks",
    "people_count": 9,
    "wait_minutes": 14,
    "pipeline_version": 2
  }
}

Heartbeat comment lines (:ping) arrive every 15s — proxies kill idle SSE around 30–60s. To resume across disconnects, send Last-Event-ID with the last id you saw; events newer than that id are replayed from a per-location 5-minute ring buffer. Older replay returns a one-shot replay_unavailable event with guidance to use /v1/snapshots for backfill. Free tier allows one concurrent stream per key.

WS/v1/stream/ws
Auth: Required

WebSocket alternative with the same payloads. SSE is the recommended path.

# Browsers can't curl a WS handshake, but here's the auth + subscribe shape:
#   wss://api.damnlines.com/v1/stream/ws
#   Sec-WebSocket-Protocol: bearer.dl_live_xxxxxxxx.yyyyyyyy...
#
# After connect, send:
#   {"type":"subscribe","locations":["salt_hanks"],"events":["snapshot","line_count"]}
Example frame
{
  "id": "1714410715123-0001",
  "event": "snapshot",
  "location": "salt_hanks",
  "captured_at": "2024-04-29T17:11:55Z",
  "data": {
    "id": "snap_aaaaddzk253xq",
    "location": "salt_hanks",
    "captured_at": "2024-04-29T17:11:55Z",
    "image_url": "https://cdn.damnlines.com/sample.jpg",
    "image_url_valid_for_seconds": 86400,
    "people_count": 12,
    "wait_minutes": 18,
    "pipeline_version": 2
  }
}

WS and SSE share payload parity, not feature parity. SSE is recommended — it's HTTP-native, curl-able, supports Authorization headers cleanly, and survives most proxies. Reach for WS only if your runtime can't hold an SSE connection.

//06 — Meta

Self-describe and machine-readable schema. Hit /v1/me to verify your key works.

GET/v1/me
Auth: Required

Describe the calling key — tier, scopes, quotas, recent usage.

curl https://api.damnlines.com/v1/me \
  -H "Authorization: Bearer dl_live_xxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
Example response
{
  "key_id": "abcd1234",
  "name": "production",
  "tier": "free",
  "scopes": [
    "read:locations",
    "read:snapshots",
    "read:videos",
    "read:lines",
    "stream:events"
  ],
  "quotas": {
    "requests_per_minute": 60,
    "requests_per_day": 5000,
    "max_window_days": 30,
    "max_replay_seconds": 300
  },
  "usage": {
    "requests_today": 1,
    "requests_this_min": 1,
    "last_used_at": null
  }
}
GET/v1/health
Auth: Public

Liveness probe. No auth, no rate limit.

curl https://api.damnlines.com/v1/health
Example response
{
  "status": "ok"
}
GET/v1/openapi.json
Auth: Public

The OpenAPI 3.1 schema for v1 — generate clients, run schemathesis, etc.

curl https://api.damnlines.com/v1/openapi.json
Shape (truncated)
{
  "openapi": "3.1.0",
  "info": {
    "title": "Damnlines API",
    "version": "1.0.0"
  },
  "paths": {
    "/v1/locations": {
      "get": {
        "summary": "List locations"
      }
    },
    "...": "..."
  }
}

Also rendered as Swagger UI at https://api.damnlines.com/v1/docs.

//07 — Errors

Every non-2xx response uses the same envelope:

{
  "error": {
    "code": "rate_limited",
    "message": "Exceeded 60 requests per minute.",
    "request_id": "01HXABCDEFG123456789"
  }
}
  • unauthorized — missing or malformed key. 401.
  • forbidden — key valid but lacks the scope. 403.
  • not_found — unknown slug, snapshot id, etc. 404.
  • validation_error — bad query, naive timestamp, malformed cursor. 400.
  • rate_limited — quota exceeded. 429 with Retry-After.
  • internal_error — something on our side. 500. Send the request_id if you file a ticket.
//EOF

Need a human? [email protected].

API Reference | damnlines