Year-in-review stories made by your app, one user at a time
By Lucency, the makers of Yearcap · Updated November 3, 2026
Some apps keep everything on the phone, so there is no spreadsheet of every user to upload. Yearcap can still give each of them a story: your app sends one user’s stats when they tap “See my year”, and opens the private story link it gets back. Here is the whole setup.
How it works
- A user taps See my 2026 in your app.
- Your app sends that user’s stats to Yearcap with your app key.
- Yearcap answers with the user’s private story link. Your app opens it in the browser or a web view.
Only that one user’s numbers leave the phone, and only when they ask. Sending the same user again updates their numbers and keeps the same link, so a tap in late December shows the whole year.
Set it up once
- Make a campaign with a small export: your own stats are enough. Its columns are the columns your app will send, and it’s where you pick the cards, the theme and your colors.
- Publish it. You land on your dashboard. Signing in first (GitHub or Google) is optional, but it keeps the dashboard in your account and tells us where to send plan emails. To try it for free, pick the Pop or Paper theme: the others need a paid plan.
- Under Let your app make each story, press Make an app key. The dashboard shows the key and the request with your own column names.
The app key can only add stories, so it’s fine to ship it inside your app. Someone who digs it out of the app could add made-up users up to your plan’s limit, though. If you see strange users on your dashboard, make a new key and the old one stops working.
Test in a separate campaign. Every user you add counts toward your plan and stays until the campaign ends, so try things in a test campaign, then delete it and set up a fresh one for your real users.
The request
POST https://yearcap.co/api/story
Authorization: Bearer yc_your_app_key
Content-Type: application/json
{
"user_id": "a1b2c3",
"first_name": "Ana",
"workouts": 146,
"kg_lifted": 12400
}- The id column you picked when you set up the campaign (here
user_id) is required. Use an id that means nothing outside your app, not an email address. - Send the same column names as your setup file. Columns you leave out keep the user’s earlier values, or start at zero.
- Numbers can be numbers or text (
"12,400"works).
CSV works too, with Content-Type: text/csv: a header line and the user’s line.
user_id,first_name,workouts,kg_lifted
a1b2c3,Ana,146,12400The answer
{
"url": "https://yearcap.co/r/your-app-x1y2/SEKgsWajQNSZ2ZVr",
"shareUrl": "https://yearcap.co/s/your-app-x1y2/aOmweiohxQbQ",
"added": true
}url is the user’s own story: open it for them. shareUrl is the public summary card their friends see when they share it. added is false when the user already had a story.
In Swift
func openYearStory(stats: [String: Any]) async throws {
var req = URLRequest(url: URL(string: "https://yearcap.co/api/story")!)
req.httpMethod = "POST"
req.setValue("Bearer \(yearcapAppKey)", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONSerialization.data(withJSONObject: stats)
let (data, response) = try await URLSession.shared.data(for: req)
guard (response as? HTTPURLResponse)?.statusCode == 200,
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let link = json["url"] as? String, let url = URL(string: link) else {
// Show "Your year isn't ready yet" and try again later.
return
}
await UIApplication.shared.open(url) // or an SFSafariViewController
}When something goes wrong
| Status | What it means | What to show the user |
|---|---|---|
| 200 | The story is ready. Open url. | The story. |
| 400 | The id column is missing, or the body isn’t one JSON object or a two-line CSV. | A bug on your side: log it. |
| 401 | The app key is wrong, or was replaced on the dashboard. | “Your year isn’t ready yet.” |
| 402 | The plan is full, or the campaign uses a paid theme on the Free plan. Users who already have a story still get theirs. | “Your year isn’t ready yet.” You get an email at 90% and when it’s full. |
| 410 | The campaign has ended: stories stay up for 90 days after you publish. | Nothing: hide the button. |
| 413 | The body is over 20 KB. Send one user at a time. | A bug on your side: log it. |
Every error comes with a short error message in the JSON, written for you, not for your users.
Plans and limits
- Each new user counts toward your plan: 500 on Free, 10,000 on Season ($49), 100,000 on Big year ($149), paid once per campaign.
- You get an email when 90% is used and when it’s full (sign in when you set up, so we know where to send it). Upgrading on the dashboard opens room right away. An upgrade is the full price of the bigger plan.
- The “top X%” card compares each user to everyone added so far. It stays hidden until 20 users have joined, then gets more accurate as more people ask.
- Stories stay up for 90 days after you publish, so set up the campaign you launch with in November or December. Then every user’s numbers and link are deleted, and the app key stops adding new ones.
Stuck?
Email support@yearcap.co and we’ll help you get it working.
Make your users’ year in about ten minutes
Drop in one spreadsheet and watch the story build live. Free up to 500 users.
Make yours, free