Day 23: Feature requests from the pilot

Day 23 of 30. Today we respond to real user feedback.

#customer-feedback #iteration

Day 23 of 30. Today we respond to real user feedback.

Our pilot has been going on now for 3 days. They’ve captured 847 screenshots and have feedback. This is absolutely great, and the moment we’ve been waiting for. These are real users telling us what they actually need.

Now, of course we’re not going to say yes to everything, and we don’t want to turn this system into a solution which is only suitable for one customer. But getting direct feedback is amazing, and certainly something we’ll take on board.

Interactive feature request board

See how we prioritized and addressed the feature requests:

features - Feature request board
[C]
Feedback from pilot customer
847 screenshots captured
[>] 24h turnaround
[OK]
Custom viewport sizes
high [T] < 1 hour
Shipped!
[OK]
Wait for selector
high [T] 30 min
Shipped!
[OK]
Thumbnail/resize
medium [T] 1 hour
Shipped!
[OK]
Response caching
medium [T] 1 hour
Shipped!
[--]
Bulk endpoint
high [T] TBD
Roadmap
4 Features shipped
1 On roadmap
< 4h Total dev time
[^] Customer happiness
[M] Customer response
"Wow, that was fast! We'll test these today. The thumbnail option alone will save us a lot of bandwidth. Getting closer to switching over fully."

The feedback

We’ve received a detailed email (slightly changed to fit the format of our blog):

The API works well for basic cases. A few things we’d love:

  1. Faster response for repeated URLs - We often screenshot the same pages. Would be nice if there was some caching.

  2. Custom viewport sizes - We need 1200x630 for Open Graph images specifically.

  3. Wait for specific element - Some of our pages load content dynamically. The “network idle” isn’t enough.

  4. Thumbnail option - We don’t always need full-resolution. A smaller image would be faster.

  5. Bulk endpoint - We sometimes need to capture 10-20 URLs at once. Individual calls incur overhead, and a faster solution would be appreciated.

Fair feedback! Let’s see what we can ship quickly.

Feature 1: Custom viewport sizes

This is easy. We already support width/height, but we weren’t advertising it well.

Updated the API:

{
  "url": "https://example.com",
  "width": 1200,
  "height": 630
}

This works with any device type. If you specify width/height, they override the device defaults.

Time to implement: Already done, just needed docs update.

Feature 2: Wait for selector

This is genuinely useful. Instead of waiting for network idle, wait for a specific element:

data class ScreenshotRequest(
    val url: String,
    val waitForSelector: String? = null,  // NEW
    // ... other fields
)

Implementation:

if (request.waitForSelector != null) {
    page.waitForSelector(request.waitForSelector,
        Page.WaitForSelectorOptions()
            .setTimeout(10000.0)
            .setState(WaitForSelectorState.VISIBLE)
    )
}

API usage:

{
  "url": "https://example.com",
  "wait_for_selector": ".main-content"
}

Now you can wait for your SPA to render specific content.

Time to implement: 30 minutes, plus time for testing, documentation and deployment.

Feature 3: Thumbnail/resize option

Returning a 3MB full-resolution image when you need a 100KB thumbnail is a little wasteful. To address this, we’ve added a resize option:

data class ScreenshotRequest(
    // ...
    val resize: ResizeOptions? = null
)

data class ResizeOptions(
    val width: Int,
    val height: Int? = null,  // Maintain aspect ratio if null
    val fit: String = "cover"  // cover, contain, fill
)

API usage:

{
  "url": "https://example.com",
  "resize": {
    "width": 400
  }
}

This now returns a 400px wide thumbnail. The response size drops from ~1MB to ~50KB.

Time to implement: 1 hour.

Feature 4: Caching

This is more complex. (There are 2 things hard in programming…)

  • Cache key: Do we use the URL only? Or the URL + screenshot options, which is more likely.
  • Cache duration: How long? A day? A week? A year?
  • Cache invalidation: How do users bypass cache?
  • Storage: Where do cached images live?

For now, we’re adding a cache parameter that returns cached results if available:

{
  "url": "https://example.com",
  "cache_ttl": 3600
}

If we captured this URL in the last hour, we return the cached result. Otherwise, we capture a fresh screenshot.

Simple implementation:

fun findCachedScreenshot(url: String, options: ScreenshotOptions, ttl: Int): Screenshot? {
    val cutoff = Instant.now().minusSeconds(ttl.toLong())
    return screenshotRepository.findByUrlAndOptionsAfter(url, options.hashCode(), cutoff)
}

Time to implement: 60 minutes, but more work will be needed in this space

Feature 5: Bulk endpoint (deferred)

Our users want to submit 10-20 URLs at once. But why stop at 20, and not go for 200, or 2000, or more? We see currently several options:

A. Sync bulk: We accept an array of URLs, process them all, and then return them all. There is a potential challenge in this, which is that it could take 60+ seconds depending on the URLs.

B. Async bulk: We also accept the array of URLs, but instead return a job ID. The client polls or uses a webhook. This is basically the async mode we designed on Day 3.

We’re deferring this for now, since it’s the trigger to build async mode properly. We told our client:

Bulk processing is on our roadmap. For now, it’s possible to parallelize on your end - our API handles concurrent requests fine. We’ll have a proper bulk endpoint in the next week.

Our client was okay with this, which is a great situation to be in!

Updated API documentation

We updated the docs with new parameters:

## Request Parameters

| Parameter         | Type    | Description               |
|-------------------|---------|---------------------------|
| url               | string  | URL to capture (required) |
| device            | string  | desktop, mobile, tablet   |
| width             | integer | Custom viewport width     |
| height            | integer | Custom viewport height    |
| full_page         | boolean | Capture entire page       |
| format            | string  | png or jpeg               |
| wait_for_selector | string  | CSS selector to wait for  |
| resize.width      | integer | Resize output width       |
| resize.height     | integer | Resize output height      |
| cache_ttl         | integer | Cache duration in seconds |

Client response

We emailed our client the updates:

Good news! We shipped three of your requests:

  • Custom viewport: Use width/height params
  • Wait for selector: Use wait_for_selector param
  • Thumbnails: Use resize param

Caching is live too. Set cache_ttl for repeated URLs.

Bulk endpoint is coming - for now, parallel requests work well.

Their reply:

Wow, that was fast! We’ll test these today. The thumbnail option alone will save us a lot of bandwidth. Getting closer to switching over fully.

This is the feedback loop working. We’re dealing with real users, real problems, and real solutions.

What we built today

  • Custom viewport documentation
  • Wait for selector feature
  • Image resizing/thumbnails
  • Basic response caching
  • Updated documentation

All shipped to production within one day.

Tomorrow: first paying customer?

On to day 24. Our client’s team has been testing for almost a week. Time to get our final feedback!

Book of the day

Inspired by Marty Cagan

Cagan runs the Silicon Valley Product Group and has worked with hundreds of product teams. This book is about building products customers actually want.

Key insight for today: the best product insights come from users, not brainstorming. Sarah’s email contained more actionable ideas than a week of us guessing what people want.

The chapter on continuous discovery - constantly talking to users and iterating - perfectly describes what we did today. Ship something, get feedback, improve, repeat.

Essential reading for anyone building products.


Day 23 stats

Hours
██████████░░░░░
68h
</> Code
██████████████░
4,600
$ Revenue
░░░░░░░░░░░░░░░
$0
Customers
░░░░░░░░░░░░░░░
0
Hosting
████░░░░░░░░░░░
$5.5/mo
Achievements:
[✓] Wait for selector added [✓] Image resizing shipped [✓] Caching implemented
╔════════════════════════════════════════════════════════════╗
E

Erik

Building Allscreenshots. Writes code, takes screenshots, goes diving.

Try allscreenshots

Screenshot API for the modern web. Capture any URL with a simple API call.

Get started