The data model
What the entities behind the API mean, how they relate, and the rules that govern who sees what. Endpoint-by-endpoint details live in the reference; runnable examples in the cookbook.
The big picture
member ─┬─ profile sections: emails, links, social media accounts,
│ addresses, phone numbers, work experiences
├─ member tags ── endorsements (given by other members)
│ └─ global tag
├─ posts ─┬─ denials (audience restrictions)
│ ├─ images
│ ├─ replies (posts themselves)
│ └─ likes · bookmarks · reposts
├─ follows → member (one-directional subscription)
├─ connections ↔ member (mutual, consented)
├─ groups (of followers) (custom audiences for posts)
└─ conversations ── messages (direct messages, request model)
Members
The central entity. A member (type: "profile" in API responses) has:
-
Identity:
name(assembled),first_name,middle_name,last_name,nickname,honorific_prefix/_suffix,gender,birthdate,locale. -
The username (
slug, e.g.wintermeyer): unique, the profile URL (vutuv.de/<slug>) and the:slugin every API path. Changeable on the website only; old usernames redirect. -
headline_markdown: the one-liner under the name, Markdown. -
verified:truewhen vutuv has verified the member's identity against a physical ID document. A trust signal — treat unverified profiles accordingly. -
counts:followers,following,connections,posts. -
Consent flags:
noindex?(member opted out of search engines) andnoai?(member opted out of AI/LLM processing) — readable on API profile responses and writable on your own profile viaPATCH /me. Honor these: if your app feeds profiles into an LLM, skip members with"noai?": true. (The public.json/.mdpages carry the same choice asContent-SignalandX-Robots-Tagresponse headers.) -
member_since: the registration date.
A member who never verified their email address, or who is currently moderated, is invisible through the API — see Visibility.
Profile sections
Each section is a list of entries belonging to one member. All of them are
readable via GET /users/:slug/<section> and (except emails) writable on
your own profile via POST/PATCH/DELETE /me/<section>:
| Section | Entry fields | Notes |
|---|---|---|
emails |
value |
Read-only over the API: an address is a PIN-verified login identity. Members mark addresses public or private; private ones are visible only to the owner (and to members the owner follows, on the website's email page). |
links |
value (URL), description |
vutuv renders a screenshot preview on the website. |
social_media_accounts |
provider, value (handle) |
Providers: Facebook, Twitter, Instagram, YouTube, Snapchat, LinkedIn, XING, GitHub. |
addresses |
description, line_1…line_4, zip_code, city, state, country |
|
phone_numbers |
value, number_type |
|
work_experiences |
title, organization, description, start_year, start_month, end_year, end_month |
An open end means "current position"; the most recent current one feeds the profile's current_position. |
Entry ids are stable — store them to update or delete later.
Tags and endorsements
-
A tag is global: one record per topic (
name+ URLslug, e.g.Phoenix→/tags/phoenix), shared by everyone who claims it. -
A member tag links a member to a tag ("Stefan claims Phoenix").
Adding a tag to your profile (
POST /me/tags) links the existing global tag or creates it on the fly. -
An endorsement is one member vouching for another member's tag.
Each member can endorse a given member-tag once. The
endorsementscount on a profile's tag entries is the member's credibility signal for that topic; profile tags sort by it.
The social graph: follows and connections
Two distinct relationships — do not conflate them:
-
A follow is one-directional and needs no consent (Twitter-style).
Following someone puts their posts into your feed. Idempotent
PUT/DELETE /users/:slug/follow. -
A connection is mutual and consented (LinkedIn-style). One side
requests, the other accepts or declines. States you will see in
relationshipresponses:none,pending_sent,pending_received,accepted,declined. Accepting a connection also materializes the follow in both directions (each side can later unfollow independently without breaking the connection). Declined requests have a cooldown before they can be re-sent; two members requesting each other auto-accepts.
GET /users/:slug/relationship answers your complete standing with one
member: following, followed_by and the connection state.
A member can also block another — deliberately opaque to the other side; see Visibility.
Groups are private, custom lists of members you follow ("colleagues", "customers"). They exist for one purpose: as building blocks for post audiences. Nobody but you sees your groups; they are managed on the website.
Posts
A post is Markdown body_markdown (up to 20,000 characters), optional
tags (the same global tags as on profiles), optional images, and an
optional audience. Posts have a permalink
(/<slug>/posts/<date>/<id>), appear in the author's archive and in their
followers' feeds.
Audiences: the denial model
Visibility is deny-based: a post with no denials is public. Each denial excludes one set of readers, and a reader matching any denial is excluded (union). Denial shapes:
-
{"wildcard": "logged_out"}— only logged-in members see it. -
{"wildcard": "non_followers"}— only your followers. -
{"wildcard": "non_followees"}— only members you follow. -
{"wildcard": "non_connections"}— only your connections. -
{"wildcard": "everyone"}— only you (a private note). -
{"denied_user_id": "<member id>"}— everyone but this member.
Once a post has replies or reposts, its audience can no longer be
restricted (409, reason: visibility_locked) — people who interacted
with a public post must not lose the context. Deleting is always possible.
Replies, likes, bookmarks, reposts
- A reply is a regular post (same fields, own permalink) attached to a public parent post; restricted posts cannot be replied to. The link to the parent survives even if the parent is deleted later.
-
Like, bookmark and repost are idempotent per-member switches
(
PUT= on,DELETE= off). A repost pushes the post into your followers' feeds (reposted_byon the feed entry); it works on public posts only. Bookmarks are private to you.
Images
Images are uploaded first (POST /me/post_images, multipart), then
attached to a post via image_ids. JPEG/PNG/WebP in, at most 6 MB and 10
per post; vutuv converts to AVIF and strips metadata. Image URLs go
through an audience-checking proxy: a restricted post's images are exactly
as restricted as the post. Unattached uploads are swept after 24 hours.
Direct messages
Messaging follows a request model that protects members from cold-DM spam:
- A conversation connects exactly two members and holds their messages (Markdown, up to 10,000 characters each).
- If the recipient follows you, your message lands directly in an accepted conversation.
-
Otherwise your first message opens a request: the recipient sees it
under
requestsand accepts or declines. Until they accept, you cannot send a second message (409,reason: pending_limit). Declining is silent — the sender keeps seeing a pending request. New requests are rate-limited per sender. -
Each side has its own unread counter;
POST /conversations/:id/readclears yours.
Notifications
A derived, read-only feed of what happened to you: new followers,
endorsements, connection requests and acceptances, replies, likes, and
moderation notices. Entries carry a kind, the acting member and a
timestamp. The unread count matches the bell icon on the website;
POST /notifications/read moves your read marker. Webhook-capable apps
get most of this pushed instead — see webhooks.
Visibility: through the member's eyes
The one rule that explains most 404s: every read returns exactly what
the authorizing member would see on the website. Visibility is enforced
server-side, per request:
-
Profiles hidden by moderation (frozen, suspended, deactivated) or never
activated:
404. - Posts: the denial model above, evaluated against the viewer.
- Emails: public addresses only, unless you are the owner.
-
Blocks: opaque —
403or404without naming the reason. - "Does not exist" and "not visible to you" are deliberately indistinguishable.
Ids, ordering, pagination
- Every id is a UUID v7 — it embeds its creation timestamp, so ids sort by creation time. Treat them as opaque strings.
-
List endpoints paginate one of two ways: cursor (feed, messages,
notifications — pass
next_cursorback as?cursor=, unmodified, untilmoreisfalse;?limit=accepts 1–100) or pages (followers/following and other browse lists —?page=N). -
Responses that mirror a public page carry
schema_version(currently1). New fields appear without notice: parse leniently, ignore unknown keys. Fields never disappear or change meaning within/api/2.0.